560 lines
18 KiB
Rust
560 lines
18 KiB
Rust
//! Symbolic execution engine.
|
|
//!
|
|
//! Explores all possible execution paths through a contract.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
use crate::ast::{BinaryOperator, Expression, Literal, Statement, UnaryOperator};
|
|
use crate::error::{VerifierError, VerifierResult};
|
|
use crate::prover::ProverConfig;
|
|
use crate::{VarType, VerificationContext};
|
|
|
|
/// Symbolic value representing an unknown or computed value.
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub enum SymbolicValue {
|
|
/// Concrete literal value.
|
|
Concrete(Literal),
|
|
/// Symbolic variable.
|
|
Symbol(String, VarType),
|
|
/// Binary operation on symbolic values.
|
|
BinaryOp {
|
|
left: Box<SymbolicValue>,
|
|
op: BinaryOperator,
|
|
right: Box<SymbolicValue>,
|
|
},
|
|
/// Unary operation.
|
|
UnaryOp {
|
|
op: UnaryOperator,
|
|
operand: Box<SymbolicValue>,
|
|
},
|
|
/// Conditional value (ITE = if-then-else).
|
|
Ite {
|
|
condition: Box<SymbolicValue>,
|
|
then_val: Box<SymbolicValue>,
|
|
else_val: Box<SymbolicValue>,
|
|
},
|
|
/// Array/mapping read.
|
|
Select {
|
|
array: Box<SymbolicValue>,
|
|
index: Box<SymbolicValue>,
|
|
},
|
|
/// Array/mapping write.
|
|
Store {
|
|
array: Box<SymbolicValue>,
|
|
index: Box<SymbolicValue>,
|
|
value: Box<SymbolicValue>,
|
|
},
|
|
}
|
|
|
|
impl SymbolicValue {
|
|
/// Creates a new symbolic variable.
|
|
pub fn symbol(name: &str, var_type: VarType) -> Self {
|
|
SymbolicValue::Symbol(name.to_string(), var_type)
|
|
}
|
|
|
|
/// Creates a concrete boolean value.
|
|
pub fn bool_const(v: bool) -> Self {
|
|
SymbolicValue::Concrete(Literal::Bool(v))
|
|
}
|
|
|
|
/// Creates a concrete uint value.
|
|
pub fn uint_const(v: u128) -> Self {
|
|
SymbolicValue::Concrete(Literal::Uint(v))
|
|
}
|
|
|
|
/// Creates a binary operation.
|
|
pub fn binary(left: SymbolicValue, op: BinaryOperator, right: SymbolicValue) -> Self {
|
|
SymbolicValue::BinaryOp {
|
|
left: Box::new(left),
|
|
op,
|
|
right: Box::new(right),
|
|
}
|
|
}
|
|
|
|
/// Creates a conditional value.
|
|
pub fn ite(condition: SymbolicValue, then_val: SymbolicValue, else_val: SymbolicValue) -> Self {
|
|
SymbolicValue::Ite {
|
|
condition: Box::new(condition),
|
|
then_val: Box::new(then_val),
|
|
else_val: Box::new(else_val),
|
|
}
|
|
}
|
|
|
|
/// Attempts to evaluate to a concrete value.
|
|
pub fn try_evaluate(&self) -> Option<Literal> {
|
|
match self {
|
|
SymbolicValue::Concrete(lit) => Some(lit.clone()),
|
|
SymbolicValue::BinaryOp { left, op, right } => {
|
|
let l = left.try_evaluate()?;
|
|
let r = right.try_evaluate()?;
|
|
Self::evaluate_binary(&l, *op, &r)
|
|
}
|
|
SymbolicValue::UnaryOp { op, operand } => {
|
|
let v = operand.try_evaluate()?;
|
|
Self::evaluate_unary(*op, &v)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn evaluate_binary(left: &Literal, op: BinaryOperator, right: &Literal) -> Option<Literal> {
|
|
match (left, op, right) {
|
|
(Literal::Uint(a), BinaryOperator::Add, Literal::Uint(b)) => {
|
|
Some(Literal::Uint(a.wrapping_add(*b)))
|
|
}
|
|
(Literal::Uint(a), BinaryOperator::Sub, Literal::Uint(b)) => {
|
|
Some(Literal::Uint(a.wrapping_sub(*b)))
|
|
}
|
|
(Literal::Uint(a), BinaryOperator::Mul, Literal::Uint(b)) => {
|
|
Some(Literal::Uint(a.wrapping_mul(*b)))
|
|
}
|
|
(Literal::Uint(a), BinaryOperator::Div, Literal::Uint(b)) if *b != 0 => {
|
|
Some(Literal::Uint(a / b))
|
|
}
|
|
(Literal::Uint(a), BinaryOperator::Eq, Literal::Uint(b)) => Some(Literal::Bool(a == b)),
|
|
(Literal::Uint(a), BinaryOperator::Lt, Literal::Uint(b)) => Some(Literal::Bool(a < b)),
|
|
(Literal::Uint(a), BinaryOperator::Le, Literal::Uint(b)) => Some(Literal::Bool(a <= b)),
|
|
(Literal::Uint(a), BinaryOperator::Gt, Literal::Uint(b)) => Some(Literal::Bool(a > b)),
|
|
(Literal::Uint(a), BinaryOperator::Ge, Literal::Uint(b)) => Some(Literal::Bool(a >= b)),
|
|
(Literal::Bool(a), BinaryOperator::And, Literal::Bool(b)) => {
|
|
Some(Literal::Bool(*a && *b))
|
|
}
|
|
(Literal::Bool(a), BinaryOperator::Or, Literal::Bool(b)) => {
|
|
Some(Literal::Bool(*a || *b))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn evaluate_unary(op: UnaryOperator, val: &Literal) -> Option<Literal> {
|
|
match (op, val) {
|
|
(UnaryOperator::Not, Literal::Bool(b)) => Some(Literal::Bool(!b)),
|
|
(UnaryOperator::Neg, Literal::Int(i)) => Some(Literal::Int(-i)),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Symbolic execution state.
|
|
#[derive(Clone, Debug)]
|
|
pub struct SymbolicState {
|
|
/// Variable bindings.
|
|
variables: HashMap<String, SymbolicValue>,
|
|
/// Path condition (constraints on this path).
|
|
path_condition: Vec<SymbolicValue>,
|
|
/// Execution depth.
|
|
depth: usize,
|
|
/// Whether this path is still feasible.
|
|
feasible: bool,
|
|
}
|
|
|
|
impl SymbolicState {
|
|
/// Creates a new empty symbolic state.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
variables: HashMap::new(),
|
|
path_condition: Vec::new(),
|
|
depth: 0,
|
|
feasible: true,
|
|
}
|
|
}
|
|
|
|
/// Gets a variable's symbolic value.
|
|
pub fn get(&self, name: &str) -> Option<&SymbolicValue> {
|
|
self.variables.get(name)
|
|
}
|
|
|
|
/// Sets a variable's symbolic value.
|
|
pub fn set(&mut self, name: &str, value: SymbolicValue) {
|
|
self.variables.insert(name.to_string(), value);
|
|
}
|
|
|
|
/// Adds a path constraint.
|
|
pub fn assume(&mut self, constraint: SymbolicValue) {
|
|
self.path_condition.push(constraint);
|
|
}
|
|
|
|
/// Returns the path condition.
|
|
pub fn path_condition(&self) -> &[SymbolicValue] {
|
|
&self.path_condition
|
|
}
|
|
|
|
/// Returns all variable bindings.
|
|
pub fn variables(&self) -> &HashMap<String, SymbolicValue> {
|
|
&self.variables
|
|
}
|
|
|
|
/// Forks the state for branching.
|
|
pub fn fork(&self) -> Self {
|
|
Self {
|
|
variables: self.variables.clone(),
|
|
path_condition: self.path_condition.clone(),
|
|
depth: self.depth,
|
|
feasible: self.feasible,
|
|
}
|
|
}
|
|
|
|
/// Increments depth.
|
|
pub fn increment_depth(&mut self) {
|
|
self.depth += 1;
|
|
}
|
|
|
|
/// Returns current depth.
|
|
pub fn depth(&self) -> usize {
|
|
self.depth
|
|
}
|
|
|
|
/// Marks path as infeasible.
|
|
pub fn mark_infeasible(&mut self) {
|
|
self.feasible = false;
|
|
}
|
|
|
|
/// Checks if path is still feasible.
|
|
pub fn is_feasible(&self) -> bool {
|
|
self.feasible
|
|
}
|
|
}
|
|
|
|
impl Default for SymbolicState {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Symbolic execution engine.
|
|
pub struct SymbolicExecutor {
|
|
config: ProverConfig,
|
|
}
|
|
|
|
impl SymbolicExecutor {
|
|
/// Creates a new symbolic executor.
|
|
pub fn new(config: ProverConfig) -> Self {
|
|
Self { config }
|
|
}
|
|
|
|
/// Creates an initial symbolic state from contract context.
|
|
pub fn create_initial_state(&self, ctx: &VerificationContext) -> VerifierResult<SymbolicState> {
|
|
let mut state = SymbolicState::new();
|
|
|
|
// Create symbolic values for state variables
|
|
for (name, var_type) in &ctx.state_vars {
|
|
let sym = SymbolicValue::symbol(name, var_type.clone());
|
|
state.set(name, sym);
|
|
}
|
|
|
|
// Add msg.sender, msg.value, etc.
|
|
state.set(
|
|
"msg.sender",
|
|
SymbolicValue::symbol("msg.sender", VarType::Address),
|
|
);
|
|
state.set(
|
|
"msg.value",
|
|
SymbolicValue::symbol("msg.value", VarType::Uint(256)),
|
|
);
|
|
state.set(
|
|
"block.timestamp",
|
|
SymbolicValue::symbol("block.timestamp", VarType::Uint(256)),
|
|
);
|
|
state.set(
|
|
"block.number",
|
|
SymbolicValue::symbol("block.number", VarType::Uint(256)),
|
|
);
|
|
|
|
Ok(state)
|
|
}
|
|
|
|
/// Explores all possible execution paths.
|
|
pub fn explore_all_paths(
|
|
&self,
|
|
ctx: &VerificationContext,
|
|
) -> VerifierResult<Vec<SymbolicState>> {
|
|
let initial = self.create_initial_state(ctx)?;
|
|
let mut completed = Vec::new();
|
|
let mut worklist = vec![initial];
|
|
|
|
while let Some(state) = worklist.pop() {
|
|
if state.depth() >= self.config.max_depth {
|
|
// Depth limit reached
|
|
completed.push(state);
|
|
continue;
|
|
}
|
|
|
|
if !state.is_feasible() {
|
|
// Path is infeasible, skip
|
|
continue;
|
|
}
|
|
|
|
// For now, just return the initial state
|
|
// Full implementation would execute each function
|
|
completed.push(state);
|
|
}
|
|
|
|
Ok(completed)
|
|
}
|
|
|
|
/// Symbolically executes a statement.
|
|
pub fn execute_statement(
|
|
&self,
|
|
stmt: &Statement,
|
|
state: &mut SymbolicState,
|
|
) -> VerifierResult<Vec<SymbolicState>> {
|
|
state.increment_depth();
|
|
|
|
if state.depth() >= self.config.max_depth {
|
|
return Err(VerifierError::Internal("Max depth exceeded".to_string()));
|
|
}
|
|
|
|
match stmt {
|
|
Statement::Assign { target, value } => {
|
|
let sym_value = self.evaluate_expression(value, state)?;
|
|
state.set(target, sym_value);
|
|
Ok(vec![state.clone()])
|
|
}
|
|
|
|
Statement::If {
|
|
condition,
|
|
then_branch,
|
|
else_branch,
|
|
} => {
|
|
let cond = self.evaluate_expression(condition, state)?;
|
|
|
|
// Fork state for both branches
|
|
let mut then_state = state.fork();
|
|
then_state.assume(cond.clone());
|
|
|
|
let mut else_state = state.fork();
|
|
else_state.assume(SymbolicValue::UnaryOp {
|
|
op: UnaryOperator::Not,
|
|
operand: Box::new(cond),
|
|
});
|
|
|
|
// Execute branches
|
|
let mut results = Vec::new();
|
|
|
|
for stmt in then_branch {
|
|
let states = self.execute_statement(stmt, &mut then_state)?;
|
|
results.extend(states);
|
|
}
|
|
|
|
for stmt in else_branch {
|
|
let states = self.execute_statement(stmt, &mut else_state)?;
|
|
results.extend(states);
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
|
|
Statement::While {
|
|
condition,
|
|
invariant: _,
|
|
body,
|
|
} => {
|
|
// Bounded loop unrolling
|
|
let mut current_states = vec![state.clone()];
|
|
let mut exit_states = Vec::new();
|
|
|
|
for _ in 0..self.config.max_loop_unroll {
|
|
let mut next_states = Vec::new();
|
|
|
|
for s in current_states.drain(..) {
|
|
let cond = self.evaluate_expression(condition, &s)?;
|
|
|
|
// Continue if condition might be true
|
|
let mut continue_state = s.fork();
|
|
continue_state.assume(cond.clone());
|
|
|
|
for stmt in body {
|
|
let results = self.execute_statement(stmt, &mut continue_state)?;
|
|
next_states.extend(results);
|
|
}
|
|
|
|
// Exit if condition might be false
|
|
let mut exit_state = s;
|
|
exit_state.assume(SymbolicValue::UnaryOp {
|
|
op: UnaryOperator::Not,
|
|
operand: Box::new(cond),
|
|
});
|
|
exit_states.push(exit_state);
|
|
}
|
|
|
|
if next_states.is_empty() {
|
|
break;
|
|
}
|
|
current_states = next_states;
|
|
}
|
|
|
|
// Combine remaining loop states with exit states
|
|
exit_states.extend(current_states);
|
|
Ok(exit_states)
|
|
}
|
|
|
|
Statement::Require(expr, _msg) => {
|
|
let cond = self.evaluate_expression(expr, state)?;
|
|
state.assume(cond);
|
|
Ok(vec![state.clone()])
|
|
}
|
|
|
|
Statement::Assert(expr) => {
|
|
let cond = self.evaluate_expression(expr, state)?;
|
|
state.assume(cond);
|
|
Ok(vec![state.clone()])
|
|
}
|
|
|
|
Statement::Assume(expr) => {
|
|
let cond = self.evaluate_expression(expr, state)?;
|
|
state.assume(cond);
|
|
Ok(vec![state.clone()])
|
|
}
|
|
|
|
Statement::Return(_) => {
|
|
// End of path
|
|
Ok(vec![state.clone()])
|
|
}
|
|
|
|
Statement::Block(stmts) => {
|
|
let mut current = state.clone();
|
|
for stmt in stmts {
|
|
let results = self.execute_statement(stmt, &mut current)?;
|
|
if results.is_empty() {
|
|
return Ok(vec![]);
|
|
}
|
|
// Take first result (simplification)
|
|
current = results.into_iter().next().unwrap();
|
|
}
|
|
Ok(vec![current])
|
|
}
|
|
|
|
_ => {
|
|
// Other statements not yet implemented
|
|
Ok(vec![state.clone()])
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Evaluates an expression to a symbolic value.
|
|
pub fn evaluate_expression(
|
|
&self,
|
|
expr: &Expression,
|
|
state: &SymbolicState,
|
|
) -> VerifierResult<SymbolicValue> {
|
|
match expr {
|
|
Expression::Literal(lit) => Ok(SymbolicValue::Concrete(lit.clone())),
|
|
|
|
Expression::Variable(name) => {
|
|
if let Some(val) = state.get(name) {
|
|
Ok(val.clone())
|
|
} else {
|
|
// Unknown variable - create fresh symbol
|
|
Ok(SymbolicValue::symbol(name, VarType::Uint(256)))
|
|
}
|
|
}
|
|
|
|
Expression::BinaryOp { left, op, right } => {
|
|
let l = self.evaluate_expression(left, state)?;
|
|
let r = self.evaluate_expression(right, state)?;
|
|
Ok(SymbolicValue::binary(l, *op, r))
|
|
}
|
|
|
|
Expression::UnaryOp { op, operand } => {
|
|
let v = self.evaluate_expression(operand, state)?;
|
|
Ok(SymbolicValue::UnaryOp {
|
|
op: *op,
|
|
operand: Box::new(v),
|
|
})
|
|
}
|
|
|
|
Expression::Ternary {
|
|
condition,
|
|
then_expr,
|
|
else_expr,
|
|
} => {
|
|
let c = self.evaluate_expression(condition, state)?;
|
|
let t = self.evaluate_expression(then_expr, state)?;
|
|
let e = self.evaluate_expression(else_expr, state)?;
|
|
Ok(SymbolicValue::ite(c, t, e))
|
|
}
|
|
|
|
Expression::MemberAccess { object, member } => {
|
|
// Simplified: create compound symbol name
|
|
let obj = self.evaluate_expression(object, state)?;
|
|
let name = format!("{:?}.{}", obj, member);
|
|
Ok(SymbolicValue::symbol(&name, VarType::Uint(256)))
|
|
}
|
|
|
|
Expression::Index { array, index } => {
|
|
let arr = self.evaluate_expression(array, state)?;
|
|
let idx = self.evaluate_expression(index, state)?;
|
|
Ok(SymbolicValue::Select {
|
|
array: Box::new(arr),
|
|
index: Box::new(idx),
|
|
})
|
|
}
|
|
|
|
Expression::Old(inner) => {
|
|
// Old values are just the initial symbolic values
|
|
self.evaluate_expression(inner, state)
|
|
}
|
|
|
|
Expression::Result => {
|
|
// Result is a special symbol
|
|
Ok(SymbolicValue::symbol("__result", VarType::Uint(256)))
|
|
}
|
|
|
|
Expression::FunctionCall { function, args: _ } => {
|
|
// Model function call result as uninterpreted function
|
|
let name = format!("{}(...)", function);
|
|
Ok(SymbolicValue::symbol(&name, VarType::Uint(256)))
|
|
}
|
|
|
|
_ => {
|
|
// Other expressions
|
|
Ok(SymbolicValue::symbol("unknown", VarType::Uint(256)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_symbolic_value_concrete() {
|
|
let v = SymbolicValue::uint_const(42);
|
|
assert!(matches!(v.try_evaluate(), Some(Literal::Uint(42))));
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbolic_value_binary() {
|
|
let a = SymbolicValue::uint_const(10);
|
|
let b = SymbolicValue::uint_const(5);
|
|
let sum = SymbolicValue::binary(a, BinaryOperator::Add, b);
|
|
|
|
assert!(matches!(sum.try_evaluate(), Some(Literal::Uint(15))));
|
|
}
|
|
|
|
#[test]
|
|
fn test_symbolic_state() {
|
|
let mut state = SymbolicState::new();
|
|
state.set("x", SymbolicValue::uint_const(100));
|
|
|
|
assert!(state.get("x").is_some());
|
|
assert!(state.get("y").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_fork() {
|
|
let mut state = SymbolicState::new();
|
|
state.set("x", SymbolicValue::uint_const(1));
|
|
|
|
let forked = state.fork();
|
|
assert!(forked.get("x").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_executor_creation() {
|
|
let executor = SymbolicExecutor::new(ProverConfig::default());
|
|
assert_eq!(executor.config.max_depth, 100);
|
|
}
|
|
}
|