#![forbid(unsafe_code)]
#![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))]
use acir::AcirField;
use acir::brillig::{
BinaryFieldOp, BinaryIntOp, ForeignCallParam, ForeignCallResult, IntegerBitSize, MemoryAddress,
Opcode,
};
use acvm_blackbox_solver::BlackBoxFunctionSolver;
use arithmetic::{BrilligArithmeticError, evaluate_binary_field_op, evaluate_binary_int_op};
use black_box::evaluate_black_box;
pub use acir::brillig;
use memory::MemoryTypeError;
pub use memory::{
FREE_MEMORY_POINTER_ADDRESS, MAX_MEMORY_SIZE, MEMORY_ADDRESSING_BIT_SIZE, Memory, MemoryValue,
STACK_POINTER_ADDRESS, offsets,
};
pub use crate::fuzzing::BranchToFeatureMap;
use crate::fuzzing::FuzzingTrace;
mod arithmetic;
mod black_box;
mod cast;
mod foreign_call;
pub mod fuzzing;
mod memory;
fn assert_usize(value: u32) -> usize {
value.try_into().expect("Failed conversion from u32 to usize")
}
fn assert_u32(value: usize) -> u32 {
value.try_into().expect("Failed conversion from usize to u32")
}
pub type ErrorCallStack = Vec<usize>;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum FailureReason {
Trap {
revert_data_offset: u32,
revert_data_size: u32,
},
RuntimeError { message: String },
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum VMStatus<F> {
Finished {
return_data_offset: u32,
return_data_size: u32,
},
InProgress,
Failure {
reason: FailureReason,
call_stack: ErrorCallStack,
},
ForeignCallWait {
function: String,
inputs: Vec<ForeignCallParam<F>>,
},
}
pub type OpcodePosition = usize;
pub type NextOpcodePositionOrState = usize;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct BrilligProfilingSample {
pub call_stack: Vec<usize>,
}
pub type BrilligProfilingSamples = Vec<BrilligProfilingSample>;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct VM<'a, F, B: BlackBoxFunctionSolver<F>> {
calldata: Vec<F>,
program_counter: usize,
foreign_call_counter: usize,
foreign_call_results: Vec<ForeignCallResult<F>>,
bytecode: &'a [Opcode<F>],
status: VMStatus<F>,
memory: Memory<F>,
call_stack: Vec<usize>,
black_box_solver: &'a B,
profiling_active: bool,
profiling_samples: BrilligProfilingSamples,
fuzzing_trace: Option<FuzzingTrace>,
}
impl<'a, F: AcirField, B: BlackBoxFunctionSolver<F>> VM<'a, F, B> {
pub fn new(
calldata: Vec<F>,
bytecode: &'a [Opcode<F>],
black_box_solver: &'a B,
profiling_active: bool,
with_branch_to_feature_map: Option<&BranchToFeatureMap>,
) -> Self {
let fuzzing_trace = with_branch_to_feature_map.cloned().map(FuzzingTrace::new);
Self {
calldata,
program_counter: 0,
foreign_call_counter: 0,
foreign_call_results: Vec::new(),
bytecode,
status: VMStatus::InProgress,
memory: Memory::default(),
call_stack: Vec::new(),
black_box_solver,
profiling_active,
profiling_samples: Vec::with_capacity(bytecode.len()),
fuzzing_trace,
}
}
pub fn is_profiling_active(&self) -> bool {
self.profiling_active
}
pub fn is_fuzzing_active(&self) -> bool {
self.fuzzing_trace.is_some()
}
pub fn take_profiling_samples(&mut self) -> BrilligProfilingSamples {
std::mem::take(&mut self.profiling_samples)
}
fn status(&mut self, status: VMStatus<F>) -> &VMStatus<F> {
self.status = status;
&self.status
}
pub fn get_status(&self) -> VMStatus<F> {
self.status.clone()
}
fn finish(&mut self, return_data_offset: u32, return_data_size: u32) -> &VMStatus<F> {
self.status(VMStatus::Finished { return_data_offset, return_data_size })
}
fn has_unprocessed_foreign_call_result(&self) -> bool {
self.foreign_call_counter < self.foreign_call_results.len()
}
pub fn resolve_foreign_call(&mut self, foreign_call_result: ForeignCallResult<F>) {
if self.has_unprocessed_foreign_call_result() {
panic!("No unresolved foreign calls; the previous results haven't been processed yet");
}
self.foreign_call_results.push(foreign_call_result);
self.status(VMStatus::InProgress);
}
fn trap(&mut self, revert_data_offset: u32, revert_data_size: u32) -> &VMStatus<F> {
self.status(VMStatus::Failure {
call_stack: self.get_call_stack(),
reason: FailureReason::Trap { revert_data_offset, revert_data_size },
})
}
fn fail(&mut self, message: String) -> &VMStatus<F> {
self.status(VMStatus::Failure {
call_stack: self.get_call_stack(),
reason: FailureReason::RuntimeError { message },
})
}
pub fn process_opcodes(&mut self) -> VMStatus<F> {
while !matches!(
self.process_opcode(),
VMStatus::Finished { .. } | VMStatus::Failure { .. } | VMStatus::ForeignCallWait { .. }
) {}
self.status.clone()
}
pub fn get_memory(&self) -> &[MemoryValue<F>] {
self.memory.values()
}
pub fn take_memory(mut self) -> Memory<F> {
std::mem::take(&mut self.memory)
}
pub fn foreign_call_counter(&self) -> usize {
self.foreign_call_counter
}
pub fn write_memory_at(&mut self, ptr: u32, value: MemoryValue<F>) {
self.memory.write(MemoryAddress::direct(ptr), value);
}
pub fn get_call_stack(&self) -> Vec<usize> {
let mut call_stack = self.get_call_stack_no_current_counter();
call_stack.push(self.program_counter);
call_stack
}
pub fn get_call_stack_no_current_counter(&self) -> Vec<usize> {
self.call_stack.clone()
}
pub fn process_opcode(&mut self) -> &VMStatus<F> {
if self.profiling_active {
let call_stack: Vec<usize> = self.get_call_stack();
self.profiling_samples.push(BrilligProfilingSample { call_stack });
}
self.process_opcode_internal()
}
pub fn get_fuzzing_trace(&self) -> Vec<u32> {
self.fuzzing_trace.as_ref().map(|trace| trace.get_trace()).unwrap_or_default()
}
fn process_opcode_internal(&mut self) -> &VMStatus<F> {
let opcode = &self.bytecode[self.program_counter];
match opcode {
Opcode::BinaryFieldOp { op, lhs, rhs, destination: result } => {
if let Err(error) = self.process_binary_field_op(*op, *lhs, *rhs, *result) {
self.fail(error.to_string())
} else {
self.increment_program_counter()
}
}
Opcode::BinaryIntOp { op, bit_size, lhs, rhs, destination: result } => {
match self.process_free_memory_op(*op, *bit_size, *lhs, *rhs, *result) {
Err(error) => return self.fail(error),
Ok(true) => return self.increment_program_counter(),
Ok(false) => {
}
}
if let Err(error) = self.process_binary_int_op(*op, *bit_size, *lhs, *rhs, *result)
{
self.fail(error.to_string())
} else {
self.increment_program_counter()
}
}
Opcode::Not { destination, source, bit_size } => {
if let Err(error) = self.process_not(*source, *destination, *bit_size) {
self.fail(error.to_string())
} else {
self.increment_program_counter()
}
}
Opcode::Cast { destination, source, bit_size } => {
let source_value = self.memory.read(*source);
let casted_value = cast::cast(source_value, *bit_size);
self.memory.write(*destination, casted_value);
self.increment_program_counter()
}
Opcode::Jump { location: destination } => self.set_program_counter(*destination),
Opcode::JumpIf { condition, location: destination } => {
let condition_value = self.memory.read(*condition);
let condition_value = match condition_value.expect_u1() {
Err(error) => {
return self.fail(format!("condition value is not a boolean: {error}"));
}
Ok(cond) => cond,
};
if condition_value {
self.fuzzing_trace_branching(*destination);
self.set_program_counter(*destination)
} else {
self.fuzzing_trace_branching(self.program_counter + 1);
self.increment_program_counter()
}
}
Opcode::CalldataCopy { destination_address, size_address, offset_address } => {
let size = assert_usize(self.memory.read(*size_address).to_u32());
let offset = assert_usize(self.memory.read(*offset_address).to_u32());
let end = if let Some(end) = offset.checked_add(size)
&& end <= self.calldata.len()
{
end
} else {
return self.fail(format!(
"CalldataCopy out of bounds: offset {offset} + size {size} \
exceeds calldata length {}",
self.calldata.len()
));
};
let values: Vec<_> = self.calldata[offset..end]
.iter()
.map(|value| MemoryValue::new_field(*value))
.collect();
self.memory.write_slice(*destination_address, &values);
self.increment_program_counter()
}
Opcode::Return => {
if let Some(return_location) = self.call_stack.pop() {
self.set_program_counter(return_location + 1)
} else {
self.fail("return opcode hit, but callstack already empty".to_string())
}
}
Opcode::ForeignCall {
function,
destinations,
destination_value_types,
inputs,
input_value_types,
} => self.process_foreign_call(
function,
destinations,
destination_value_types,
inputs,
input_value_types,
),
Opcode::Mov { destination: destination_address, source: source_address } => {
let source_value = self.memory.read(*source_address);
self.memory.write(*destination_address, source_value);
self.increment_program_counter()
}
Opcode::ConditionalMov { destination, source_a, source_b, condition } => {
let condition_value = self.memory.read(*condition);
let condition_value = match condition_value.expect_u1() {
Err(error) => {
return self.fail(format!("condition value is not a boolean: {error}"));
}
Ok(cond) => cond,
};
if condition_value {
self.memory.write(*destination, self.memory.read(*source_a));
} else {
self.memory.write(*destination, self.memory.read(*source_b));
}
self.fuzzing_trace_conditional_mov(condition_value);
self.increment_program_counter()
}
Opcode::Trap { revert_data } => {
let revert_data_size = self.memory.read(revert_data.size).to_u32();
if revert_data_size > 0 {
self.trap(
self.memory.read_ref(revert_data.pointer).unwrap_direct(),
revert_data_size,
)
} else {
self.trap(0, 0)
}
}
Opcode::Stop { return_data } => {
let return_data_size = self.memory.read(return_data.size).to_u32();
if return_data_size > 0 {
self.finish(
self.memory.read_ref(return_data.pointer).unwrap_direct(),
return_data_size,
)
} else {
self.finish(0, 0)
}
}
Opcode::Load { destination, source_pointer } => {
let source = self.memory.read_ref(*source_pointer);
let value = self.memory.read(source);
self.memory.write(*destination, value);
self.increment_program_counter()
}
Opcode::Store { destination_pointer, source: source_address } => {
let destination = self.memory.read_ref(*destination_pointer);
let value = self.memory.read(*source_address);
self.memory.write(destination, value);
self.increment_program_counter()
}
Opcode::Call { location } => {
self.call_stack.push(self.program_counter);
self.set_program_counter(*location)
}
Opcode::Const { destination, value, bit_size } => {
self.memory.write(*destination, MemoryValue::new_from_field(*value, *bit_size));
self.increment_program_counter()
}
Opcode::IndirectConst { destination_pointer, bit_size, value } => {
let destination = self.memory.read_ref(*destination_pointer);
self.memory.write(destination, MemoryValue::new_from_field(*value, *bit_size));
self.increment_program_counter()
}
Opcode::BlackBox(black_box_op) => {
if let Err(e) =
evaluate_black_box(black_box_op, self.black_box_solver, &mut self.memory)
{
self.fail(e.to_string())
} else {
self.increment_program_counter()
}
}
}
}
pub fn program_counter(&self) -> usize {
self.program_counter
}
fn increment_program_counter(&mut self) -> &VMStatus<F> {
self.set_program_counter(self.program_counter + 1)
}
fn set_program_counter(&mut self, value: usize) -> &VMStatus<F> {
assert!(self.program_counter < self.bytecode.len());
self.program_counter = value;
if self.program_counter >= self.bytecode.len() {
self.status = VMStatus::Finished { return_data_offset: 0, return_data_size: 0 };
}
&self.status
}
fn process_binary_field_op(
&mut self,
op: BinaryFieldOp,
lhs: MemoryAddress,
rhs: MemoryAddress,
result: MemoryAddress,
) -> Result<(), BrilligArithmeticError> {
let lhs_value = self.memory.read(lhs);
let rhs_value = self.memory.read(rhs);
let result_value = evaluate_binary_field_op(&op, lhs_value, rhs_value)?;
self.memory.write(result, result_value);
self.fuzzing_trace_binary_field_op_comparison(&op, lhs_value, rhs_value, result_value);
Ok(())
}
fn process_binary_int_op(
&mut self,
op: BinaryIntOp,
bit_size: IntegerBitSize,
lhs: MemoryAddress,
rhs: MemoryAddress,
result: MemoryAddress,
) -> Result<(), BrilligArithmeticError> {
let lhs_value = self.memory.read(lhs);
let rhs_value = self.memory.read(rhs);
let result_value = evaluate_binary_int_op(&op, lhs_value, rhs_value, bit_size)?;
self.memory.write(result, result_value);
self.fuzzing_trace_binary_int_op_comparison(&op, lhs_value, rhs_value, result_value);
Ok(())
}
fn process_free_memory_op(
&mut self,
op: BinaryIntOp,
bit_size: IntegerBitSize,
lhs: MemoryAddress,
rhs: MemoryAddress,
result: MemoryAddress,
) -> Result<bool, String> {
if result != FREE_MEMORY_POINTER_ADDRESS
|| op != BinaryIntOp::Add
|| bit_size != MEMORY_ADDRESSING_BIT_SIZE
{
return Ok(false);
}
let lhs_value = self.memory.read(lhs);
let rhs_value = self.memory.read(rhs);
let MemoryValue::U32(lhs_value) = lhs_value else {
return Ok(false);
};
let MemoryValue::U32(rhs_value) = rhs_value else {
return Ok(false);
};
let Some(result_value) = lhs_value.checked_add(rhs_value) else {
return Err("Out of memory".to_string());
};
self.memory.write(result, result_value.into());
Ok(true)
}
fn process_not(
&mut self,
source: MemoryAddress,
destination: MemoryAddress,
op_bit_size: IntegerBitSize,
) -> Result<(), MemoryTypeError> {
let value = self.memory.read(source);
let negated_value = match op_bit_size {
IntegerBitSize::U1 => MemoryValue::U1(!value.expect_u1()?),
IntegerBitSize::U8 => MemoryValue::U8(!value.expect_u8()?),
IntegerBitSize::U16 => MemoryValue::U16(!value.expect_u16()?),
IntegerBitSize::U32 => MemoryValue::U32(!value.expect_u32()?),
IntegerBitSize::U64 => MemoryValue::U64(!value.expect_u64()?),
IntegerBitSize::U128 => MemoryValue::U128(!value.expect_u128()?),
};
self.memory.write(destination, negated_value);
Ok(())
}
}