Skip to content

Commit

Permalink
zero-cost tracers
Browse files Browse the repository at this point in the history
Tracers add code to instructions directly, so the performance impact is minimized.

Introduces a lot of ugliness that needs to be addressed:
 - World is now World<T> because otherwise it isn't object safe
 - &PANIC is now &*vm.panic because panic's code depends on the tracer
 - UNIMPLEMENTED_INSTRUCTION isn't quite as bad because of questionable
   unsafe code
  • Loading branch information
joonazan committed Aug 6, 2024
1 parent 331253d commit 23a782b
Show file tree
Hide file tree
Showing 46 changed files with 674 additions and 450 deletions.
2 changes: 1 addition & 1 deletion afl-fuzz/src/check_input_size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ fn main() {
const BYTES_GIVEN: usize = 10000;
let data = [0xFF; BYTES_GIVEN];
let mut u = arbitrary::Unstructured::new(&data);
let _: VmAndWorld = u.arbitrary().unwrap();
let _: VmAndWorld<()> = u.arbitrary().unwrap();
println!("{:?}", BYTES_GIVEN - u.len());
}
4 changes: 2 additions & 2 deletions afl-fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use arbitrary::Arbitrary;
use vm2::{single_instruction_test::MockWorld, VirtualMachine};

#[derive(Arbitrary, Debug)]
pub struct VmAndWorld {
pub vm: VirtualMachine,
pub struct VmAndWorld<T> {
pub vm: VirtualMachine<T>,
pub world: MockWorld,
}
2 changes: 1 addition & 1 deletion afl-fuzz/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn main() {

let mut zk_evm = vm2_to_zk_evm(&vm, world.clone());

vm.run_single_instruction(&mut world);
vm.run_single_instruction(&mut world, &mut ());
assert!(vm.is_in_valid_state());

add_heap_to_zk_evm(&mut zk_evm, &vm);
Expand Down
2 changes: 1 addition & 1 deletion afl-fuzz/src/show_testcase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn main() {
vm.state.current_frame.raw_first_instruction(),
);
println!("{}", parsed);
vm.run_single_instruction(&mut world);
vm.run_single_instruction(&mut world, &mut ());

println!("Mocks that have been touched:");
vm.print_mock_info();
Expand Down
4 changes: 2 additions & 2 deletions benches/nested_near_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ fn nested_near_call(bencher: Bencher) {
},
);

vm.run(black_box(&mut world));
vm.run(black_box(&mut world), &mut ());
});
}

Expand Down Expand Up @@ -87,7 +87,7 @@ fn nested_near_call_with_storage_write(bencher: Bencher) {
},
);

vm.run(black_box(&mut world));
vm.run(black_box(&mut world), &mut ());
});
}

Expand Down
140 changes: 4 additions & 136 deletions eravm-stable-interface/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,136 +1,4 @@
use primitive_types::{H160, U256};

pub trait StateInterface {
fn read_register(&self, register: u8) -> (U256, bool);
fn set_register(&mut self, register: u8, value: U256, is_pointer: bool);

fn number_of_callframes(&self) -> usize;
/// zero is the current frame, one is the frame before that etc.
fn callframe(&mut self, n: usize) -> impl CallframeInterface + '_;

fn read_heap_byte(&self, heap: HeapId, index: u32) -> u8;
fn write_heap_byte(&mut self, heap: HeapId, index: u32, byte: u8);

fn flags(&self) -> Flags;
fn set_flags(&mut self, flags: Flags);

fn transaction_number(&self) -> u16;
fn set_transaction_number(&mut self, value: u16);

fn context_u128_register(&self) -> u128;
fn set_context_u128_register(&mut self, value: u128);

fn get_storage_state(&self) -> impl Iterator<Item = ((H160, U256), U256)>;
/// Returns the new value and the amount of pubdata paid for it.
fn get_storage(&self, address: H160, slot: U256) -> Option<(U256, u32)>;
fn get_storage_initial_value(&self, address: H160, slot: U256) -> U256;
fn write_storage(&mut self, address: H160, slot: U256, value: U256);

fn get_transient_storage_state(&self) -> impl Iterator<Item = ((H160, U256), U256)>;
fn get_transient_storage(&self, address: H160, slot: U256) -> U256;
fn write_transient_storage(&mut self, address: H160, slot: U256, value: U256);

fn events(&self) -> impl Iterator<Item = Event>;
fn l2_to_l1_logs(&self) -> impl Iterator<Item = L2ToL1Log>;

fn pubdata(&self) -> i32;
fn set_pubdata(&mut self, value: i32);

/// it is run in kernel mode
fn run_arbitrary_code(code: &[u64]);

fn static_heap(&self) -> HeapId;
}

pub struct Flags {
pub less_than: bool,
pub equal: bool,
pub greater: bool,
}

pub trait CallframeInterface {
fn address(&self) -> H160;
fn set_address(&mut self, address: H160);
fn code_address(&self) -> H160;
fn set_code_address(&mut self, address: H160);
fn caller(&self) -> H160;
fn set_caller(&mut self, address: H160);

/// During panic and arbitrary code execution this returns None.
fn program_counter(&self) -> Option<u16>;

/// The VM will execute an invalid instruction if you jump out of the program.
fn set_program_counter(&mut self, value: u16);

fn exception_handler(&self) -> u16;

fn is_static(&self) -> bool;
fn is_kernel(&self) -> bool {
todo!()
}

fn gas(&self) -> u32;
fn set_gas(&mut self, new_gas: u32);
fn stipend(&self) -> u32;

fn context_u128(&self) -> u128;
fn set_context_u128(&mut self, value: u128);

fn is_near_call(&self) -> bool;

fn read_stack(&self, index: u16) -> (U256, bool);
fn write_stack(&mut self, index: u16, value: U256, is_pointer: bool);

fn stack_pointer(&self) -> u16;
fn set_stack_pointer(&mut self, value: u16);

fn heap(&self) -> HeapId;
fn heap_bound(&self) -> u32;
fn set_heap_bound(&mut self, value: u32);

fn aux_heap(&self) -> HeapId;
fn aux_heap_bound(&self) -> u32;
fn set_aux_heap_bound(&mut self, value: u32);

fn read_code_page(&self, slot: u16) -> U256;
}

pub trait Tracer {
fn before_instruction<S: StateInterface>(&mut self, state: &mut S);
fn after_instruction<S: StateInterface>(&mut self, state: &mut S);
}

#[derive(Copy, Clone, PartialEq, Debug)]
pub struct HeapId(u32);

impl HeapId {
/// Only for dealing with external data structures, never use internally.
pub const fn from_u32_unchecked(value: u32) -> Self {
Self(value)
}

pub const fn to_u32(self) -> u32 {
self.0
}
}

/// There is no address field because nobody is interested in events that don't come
/// from the event writer, so we simply do not record events coming frome anywhere else.
#[derive(Clone, PartialEq, Debug)]
pub struct Event {
pub key: U256,
pub value: U256,
pub is_first: bool,
pub shard_id: u8,
pub tx_number: u16,
}

#[derive(Debug)]
pub struct L2ToL1Log {
pub key: U256,
pub value: U256,
pub is_service: bool,
pub address: H160,
pub shard_id: u8,
pub tx_number: u16,
}
mod state_interface;
mod tracer_interface;
pub use state_interface::*;
pub use tracer_interface::*;
131 changes: 131 additions & 0 deletions eravm-stable-interface/src/state_interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use primitive_types::{H160, U256};

pub trait StateInterface {
fn read_register(&self, register: u8) -> (U256, bool);
fn set_register(&mut self, register: u8, value: U256, is_pointer: bool);

fn number_of_callframes(&self) -> usize;
/// zero is the current frame, one is the frame before that etc.
fn callframe(&mut self, n: usize) -> impl CallframeInterface + '_;

fn read_heap_byte(&self, heap: HeapId, index: u32) -> u8;
fn write_heap_byte(&mut self, heap: HeapId, index: u32, byte: u8);

fn flags(&self) -> Flags;
fn set_flags(&mut self, flags: Flags);

fn transaction_number(&self) -> u16;
fn set_transaction_number(&mut self, value: u16);

fn context_u128_register(&self) -> u128;
fn set_context_u128_register(&mut self, value: u128);

fn get_storage_state(&self) -> impl Iterator<Item = ((H160, U256), U256)>;
/// Returns the new value and the amount of pubdata paid for it.
fn get_storage(&self, address: H160, slot: U256) -> Option<(U256, u32)>;
fn get_storage_initial_value(&self, address: H160, slot: U256) -> U256;
fn write_storage(&mut self, address: H160, slot: U256, value: U256);

fn get_transient_storage_state(&self) -> impl Iterator<Item = ((H160, U256), U256)>;
fn get_transient_storage(&self, address: H160, slot: U256) -> U256;
fn write_transient_storage(&mut self, address: H160, slot: U256, value: U256);

fn events(&self) -> impl Iterator<Item = Event>;
fn l2_to_l1_logs(&self) -> impl Iterator<Item = L2ToL1Log>;

fn pubdata(&self) -> i32;
fn set_pubdata(&mut self, value: i32);

/// it is run in kernel mode
fn run_arbitrary_code(code: &[u64]);

fn static_heap(&self) -> HeapId;
}

pub struct Flags {
pub less_than: bool,
pub equal: bool,
pub greater: bool,
}

pub trait CallframeInterface {
fn address(&self) -> H160;
fn set_address(&mut self, address: H160);
fn code_address(&self) -> H160;
fn set_code_address(&mut self, address: H160);
fn caller(&self) -> H160;
fn set_caller(&mut self, address: H160);

/// During panic and arbitrary code execution this returns None.
fn program_counter(&self) -> Option<u16>;

/// The VM will execute an invalid instruction if you jump out of the program.
fn set_program_counter(&mut self, value: u16);

fn exception_handler(&self) -> u16;

fn is_static(&self) -> bool;
fn is_kernel(&self) -> bool {
todo!()
}

fn gas(&self) -> u32;
fn set_gas(&mut self, new_gas: u32);
fn stipend(&self) -> u32;

fn context_u128(&self) -> u128;
fn set_context_u128(&mut self, value: u128);

fn is_near_call(&self) -> bool;

fn read_stack(&self, index: u16) -> (U256, bool);
fn write_stack(&mut self, index: u16, value: U256, is_pointer: bool);

fn stack_pointer(&self) -> u16;
fn set_stack_pointer(&mut self, value: u16);

fn heap(&self) -> HeapId;
fn heap_bound(&self) -> u32;
fn set_heap_bound(&mut self, value: u32);

fn aux_heap(&self) -> HeapId;
fn aux_heap_bound(&self) -> u32;
fn set_aux_heap_bound(&mut self, value: u32);

fn read_code_page(&self, slot: u16) -> U256;
}

#[derive(Copy, Clone, PartialEq, Debug)]
pub struct HeapId(u32);

impl HeapId {
/// Only for dealing with external data structures, never use internally.
pub const fn from_u32_unchecked(value: u32) -> Self {
Self(value)
}

pub const fn to_u32(self) -> u32 {
self.0
}
}

/// There is no address field because nobody is interested in events that don't come
/// from the event writer, so we simply do not record events coming frome anywhere else.
#[derive(Clone, PartialEq, Debug)]
pub struct Event {
pub key: U256,
pub value: U256,
pub is_first: bool,
pub shard_id: u8,
pub tx_number: u16,
}

#[derive(Debug)]
pub struct L2ToL1Log {
pub key: U256,
pub value: U256,
pub is_service: bool,
pub address: H160,
pub shard_id: u8,
pub tx_number: u16,
}
38 changes: 38 additions & 0 deletions eravm-stable-interface/src/tracer_interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::StateInterface;

pub mod opcodes {
pub struct Nop;
pub struct NearCall;
pub struct FarCall;
pub struct Ret;
pub struct Jump;
pub struct Event;
pub struct L2ToL1Message;
pub struct Decommit;
pub struct ContextMeta;
pub struct SetContextU128;
pub struct IncrementTxNumber;
pub struct AuxMutating0;
pub struct PrecompileCall;
pub struct HeapRead;
pub struct HeapWrite;
pub struct PointerRead;
pub struct StorageRead;
pub struct StorageWrite;
pub struct TransientStorageRead;
pub struct TransientStorageWrite;
pub struct StaticMemoryRead;
pub struct StaticMemoryWrite;
}

pub trait Tracer<Opcode> {
#[inline(always)]
fn before_instruction<S: StateInterface>(&mut self, _state: &mut S) {}

#[inline(always)]
fn after_instruction<S: StateInterface>(&mut self, _state: &mut S) {}
}

// The &mut is a workaround for the lack of specialization in stable Rust.
// Trait resolution will choose an implementation on U over the implementation on `&mut U`.
impl<T, U> Tracer<T> for &mut U {}
Loading

0 comments on commit 23a782b

Please sign in to comment.