Skip to content

Commit

Permalink
move code from dispatch to boilerplate
Browse files Browse the repository at this point in the history
  • Loading branch information
joonazan committed Aug 26, 2024
1 parent c640a06 commit e448ac7
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 194 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ harness = false

[features]
default = []
trace = []
single_instruction_test = ["arbitrary", "u256/arbitrary", "zk_evm", "anyhow"]

[workspace]
Expand Down
45 changes: 27 additions & 18 deletions src/instruction_handlers/common.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::free_panic;
use crate::{addressing_modes::Arguments, instruction::ExecutionStatus, VirtualMachine};
use eravm_stable_interface::{OpcodeType, Tracer};
use eravm_stable_interface::{opcodes, OpcodeType, Tracer};

#[inline(always)]
pub(crate) fn instruction_boilerplate<Opcode: OpcodeType, T: Tracer, W>(
Expand All @@ -8,15 +9,10 @@ pub(crate) fn instruction_boilerplate<Opcode: OpcodeType, T: Tracer, W>(
tracer: &mut T,
business_logic: impl FnOnce(&mut VirtualMachine<T, W>, &Arguments, &mut W),
) -> ExecutionStatus {
tracer.before_instruction::<Opcode, _>(vm);
unsafe {
let instruction = vm.state.current_frame.pc;
vm.state.current_frame.pc = instruction.add(1);
business_logic(vm, &(*instruction).arguments, world);
};
tracer.after_instruction::<Opcode, _>(vm);

ExecutionStatus::Running
instruction_boilerplate_ext::<Opcode, T, W>(vm, world, tracer, |vm, args, _, world| {
business_logic(vm, args, world);
ExecutionStatus::Running
})
}

#[inline(always)]
Expand All @@ -31,14 +27,27 @@ pub(crate) fn instruction_boilerplate_ext<Opcode: OpcodeType, T: Tracer, W>(
&mut W,
) -> ExecutionStatus,
) -> ExecutionStatus {
tracer.before_instruction::<Opcode, _>(vm);
let result = unsafe {
let instruction = vm.state.current_frame.pc;
vm.state.current_frame.pc = instruction.add(1);
let args = unsafe { &(*vm.state.current_frame.pc).arguments };

business_logic(vm, &(*instruction).arguments, tracer, world)
};
tracer.after_instruction::<Opcode, _>(vm);
if vm.state.use_gas(args.get_static_gas_cost()).is_err()
|| !args.mode_requirements().met(
vm.state.current_frame.is_kernel,
vm.state.current_frame.is_static,
)
{
return free_panic(vm, tracer);
}

result
if args.predicate().satisfied(&vm.state.flags) {
tracer.before_instruction::<Opcode, _>(vm);
vm.state.current_frame.pc = unsafe { vm.state.current_frame.pc.add(1) };
let result = business_logic(vm, args, tracer, world);
tracer.after_instruction::<Opcode, _>(vm);
result
} else {
tracer.before_instruction::<opcodes::Nop, _>(vm);
vm.state.current_frame.pc = unsafe { vm.state.current_frame.pc.add(1) };
tracer.after_instruction::<opcodes::Nop, _>(vm);
ExecutionStatus::Running
}
}
220 changes: 116 additions & 104 deletions src/instruction_handlers/ret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,111 +13,140 @@ use eravm_stable_interface::{
};
use u256::U256;

fn ret<T: Tracer, W, RT: TypeLevelReturnType, const TO_LABEL: bool>(
fn naked_ret<T: Tracer, W, RT: TypeLevelReturnType, const TO_LABEL: bool>(
vm: &mut VirtualMachine<T, W>,
world: &mut W,
tracer: &mut T,
args: &Arguments,
) -> ExecutionStatus {
instruction_boilerplate_ext::<opcodes::Ret<RT>, _, _>(vm, world, tracer, |vm, args, _, _| {
let mut return_type = RT::VALUE;
let near_call_leftover_gas = vm.state.current_frame.gas;
let mut return_type = RT::VALUE;
let near_call_leftover_gas = vm.state.current_frame.gas;

let (snapshot, leftover_gas) = if let Some(FrameRemnant {
exception_handler,
snapshot,
}) = vm.state.current_frame.pop_near_call()
{
if TO_LABEL {
let pc = Immediate1::get(args, &mut vm.state).low_u32() as u16;
vm.state.current_frame.set_pc_from_u16(pc);
} else if return_type.is_failure() {
vm.state.current_frame.set_pc_from_u16(exception_handler)
}

let (snapshot, leftover_gas) = if let Some(FrameRemnant {
exception_handler,
snapshot,
}) = vm.state.current_frame.pop_near_call()
{
if TO_LABEL {
let pc = Immediate1::get(args, &mut vm.state).low_u32() as u16;
vm.state.current_frame.set_pc_from_u16(pc);
} else if return_type.is_failure() {
vm.state.current_frame.set_pc_from_u16(exception_handler)
(snapshot, near_call_leftover_gas)
} else {
let return_value_or_panic = if return_type == ReturnType::Panic {
None
} else {
let (raw_abi, is_pointer) = Register1::get_with_pointer_flag(args, &mut vm.state);
let result = get_far_call_calldata(raw_abi, is_pointer, vm, false).filter(|pointer| {
vm.state.current_frame.is_kernel
|| pointer.memory_page != vm.state.current_frame.calldata_heap
});

if result.is_none() {
return_type = ReturnType::Panic;
}
result
};

(snapshot, near_call_leftover_gas)
} else {
let return_value_or_panic = if return_type == ReturnType::Panic {
None
} else {
let (raw_abi, is_pointer) = Register1::get_with_pointer_flag(args, &mut vm.state);
let result =
get_far_call_calldata(raw_abi, is_pointer, vm, false).filter(|pointer| {
vm.state.current_frame.is_kernel
|| pointer.memory_page != vm.state.current_frame.calldata_heap
});

if result.is_none() {
return_type = ReturnType::Panic;
}
result
};
let leftover_gas = vm
.state
.current_frame
.gas
.saturating_sub(vm.state.current_frame.stipend);

let leftover_gas = vm
.state
.current_frame
.gas
.saturating_sub(vm.state.current_frame.stipend);

let Some(FrameRemnant {
exception_handler,
snapshot,
}) = vm.pop_frame(
return_value_or_panic
.as_ref()
.map(|pointer| pointer.memory_page),
)
else {
// The initial frame is not rolled back, even if it fails.
// It is the caller's job to clean up when the execution as a whole fails because
// the caller may take external snapshots while the VM is in the initial frame and
// these would break were the initial frame to be rolled back.

// But to continue execution would be nonsensical and can cause UB because there
// is no next instruction after a panic arising from some other instruction.
vm.state.current_frame.pc = invalid_instruction();

return if let Some(return_value) = return_value_or_panic {
let output = vm.state.heaps[return_value.memory_page]
.read_range_big_endian(
return_value.start..return_value.start + return_value.length,
)
.to_vec();
if return_type == ReturnType::Revert {
ExecutionStatus::Stopped(ExecutionEnd::Reverted(output))
} else {
ExecutionStatus::Stopped(ExecutionEnd::ProgramFinished(output))
}
let Some(FrameRemnant {
exception_handler,
snapshot,
}) = vm.pop_frame(
return_value_or_panic
.as_ref()
.map(|pointer| pointer.memory_page),
)
else {
// The initial frame is not rolled back, even if it fails.
// It is the caller's job to clean up when the execution as a whole fails because
// the caller may take external snapshots while the VM is in the initial frame and
// these would break were the initial frame to be rolled back.

// But to continue execution would be nonsensical and can cause UB because there
// is no next instruction after a panic arising from some other instruction.
vm.state.current_frame.pc = invalid_instruction();

return if let Some(return_value) = return_value_or_panic {
let output = vm.state.heaps[return_value.memory_page]
.read_range_big_endian(
return_value.start..return_value.start + return_value.length,
)
.to_vec();
if return_type == ReturnType::Revert {
ExecutionStatus::Stopped(ExecutionEnd::Reverted(output))
} else {
ExecutionStatus::Stopped(ExecutionEnd::Panicked)
};
ExecutionStatus::Stopped(ExecutionEnd::ProgramFinished(output))
}
} else {
ExecutionStatus::Stopped(ExecutionEnd::Panicked)
};
};

vm.state.set_context_u128(0);
vm.state.registers = [U256::zero(); 16];

if let Some(return_value) = return_value_or_panic {
vm.state.registers[1] = return_value.into_u256();
}
vm.state.register_pointer_flags = 2;

if return_type.is_failure() {
vm.state.current_frame.set_pc_from_u16(exception_handler)
}
vm.state.set_context_u128(0);
vm.state.registers = [U256::zero(); 16];

(snapshot, leftover_gas)
};
if let Some(return_value) = return_value_or_panic {
vm.state.registers[1] = return_value.into_u256();
}
vm.state.register_pointer_flags = 2;

if return_type.is_failure() {
vm.world_diff.rollback(snapshot);
vm.state.current_frame.set_pc_from_u16(exception_handler)
}

vm.state.flags = Flags::new(return_type == ReturnType::Panic, false, false);
vm.state.current_frame.gas += leftover_gas;
(snapshot, leftover_gas)
};

if return_type.is_failure() {
vm.world_diff.rollback(snapshot);
}

ExecutionStatus::Running
vm.state.flags = Flags::new(return_type == ReturnType::Panic, false, false);
vm.state.current_frame.gas += leftover_gas;

ExecutionStatus::Running
}

fn ret<T: Tracer, W, RT: TypeLevelReturnType, const TO_LABEL: bool>(
vm: &mut VirtualMachine<T, W>,
world: &mut W,
tracer: &mut T,
) -> ExecutionStatus {
instruction_boilerplate_ext::<opcodes::Ret<RT>, _, _>(vm, world, tracer, |vm, args, _, _| {
naked_ret::<T, W, RT, TO_LABEL>(vm, args)
})
}

/// Turn the current instruction into a panic at no extra cost. (Great value, I know.)
///
/// Call this when:
/// - gas runs out when paying for the fixed cost of an instruction
/// - causing side effects in a static context
/// - using privileged instructions while not in a system call
/// - the far call stack overflows
///
/// For all other panics, point the instruction pointer at [PANIC] instead.
pub(crate) fn free_panic<T: Tracer, W>(
vm: &mut VirtualMachine<T, W>,
tracer: &mut T,
) -> ExecutionStatus {
tracer.before_instruction::<opcodes::Ret<Panic>, _>(vm);
// args aren't used for panics unless TO_LABEL
let result = naked_ret::<T, W, opcodes::Panic, false>(
vm,
&Arguments::new(Predicate::Always, 0, ModeRequirements::none()),
);
tracer.before_instruction::<opcodes::Ret<Panic>, _>(vm);
result
}

/// Formally, a far call pushes a new frame and returns from it immediately if it panics.
/// This function instead panics without popping a frame to save on allocation.
pub(crate) fn panic_from_failed_far_call<T: Tracer, W>(
Expand Down Expand Up @@ -152,23 +181,6 @@ pub fn invalid_instruction<'a, T, W>() -> &'a Instruction<T, W> {

pub(crate) const RETURN_COST: u32 = 5;

/// Turn the current instruction into a panic at no extra cost. (Great value, I know.)
///
/// Call this when:
/// - gas runs out when paying for the fixed cost of an instruction
/// - causing side effects in a static context
/// - using privileged instructions while not in a system call
/// - the far call stack overflows
///
/// For all other panics, point the instruction pointer at [PANIC] instead.
pub(crate) fn free_panic<T: Tracer, W>(
vm: &mut VirtualMachine<T, W>,
world: &mut W,
tracer: &mut T,
) -> ExecutionStatus {
ret::<T, W, opcodes::Panic, false>(vm, world, tracer)
}

use super::monomorphization::*;
use eravm_stable_interface::opcodes::{Normal, Panic, Revert};

Expand Down
Loading

0 comments on commit e448ac7

Please sign in to comment.