Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions crates/evm/symbolic/src/runtime/expr/bool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,38 +800,6 @@ impl SymBoolExpr {
ControlFlow::Continue(())
});
}

pub(crate) fn smt(&self, cx: &SymCx) -> String {
let mut smt = String::new();
self.write_smt(cx, &mut smt);
smt
}

pub(in crate::runtime::expr) fn write_smt(&self, cx: &SymCx, out: &mut String) {
match self.kind() {
SymBoolExprKind::Const(value) => out.push_str(if *value { "true" } else { "false" }),
SymBoolExprKind::Not(value) => {
out.push_str("(not ");
value.write_smt(cx, out);
out.push(')');
}
SymBoolExprKind::And(values) => {
out.push_str("(and");
for value in values.iter() {
out.push(' ');
value.write_smt(cx, out);
}
out.push(')');
}
SymBoolExprKind::Cmp(op, left, right) => {
let _ = write!(out, "({} ", op.smt());
left.write_smt(cx, out);
out.push(' ');
right.write_smt(cx, out);
out.push(')');
}
}
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
Expand Down
61 changes: 0 additions & 61 deletions crates/evm/symbolic/src/runtime/expr/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2059,42 +2059,6 @@ impl SymExpr {
folded.words.insert(self, expr.clone());
expr
}

pub(in crate::runtime::expr) fn write_smt(&self, cx: &SymCx, out: &mut String) {
match self.kind() {
SymExprKind::Const(value) => {
let _ = write!(out, "(_ bv{value} 256)");
}
SymExprKind::Var(symbol)
| SymExprKind::GasLeft(symbol)
| SymExprKind::Keccak { name: symbol, .. }
| SymExprKind::Hash { name: symbol, .. } => out.push_str(cx.symbol_name(*symbol)),
SymExprKind::Not(value) => {
out.push_str("(bvnot ");
value.write_smt(cx, out);
out.push(')');
}
SymExprKind::BinOp(op, left, right) => {
let _ = write!(out, "({} ", op.smt());
left.write_smt(cx, out);
out.push(' ');
right.write_smt(cx, out);
out.push(')');
}
SymExprKind::TernOp(op, left, right, modulus) => {
write_smt_wide_modular_arithmetic(cx, out, op.smt(), left, right, modulus);
}
SymExprKind::Ite(cond, left, right) => {
out.push_str("(ite ");
cond.write_smt(cx, out);
out.push(' ');
left.write_smt(cx, out);
out.push(' ');
right.write_smt(cx, out);
out.push(')');
}
}
}
}

// Branchless expression rewrites are optional. Bound both the distinct DAG nodes inspected while
Expand Down Expand Up @@ -2195,31 +2159,6 @@ impl UnfoldedNodeCounter {
}
}

fn write_smt_wide_modular_arithmetic(
cx: &SymCx,
out: &mut String,
op: &'static str,
left: &SymExpr,
right: &SymExpr,
modulus: &SymExpr,
) {
// if modulus == 0:
// 0
// else:
// low_256((zext(left) op zext(right)) urem zext(modulus))
out.push_str("(ite (= ");
modulus.write_smt(cx, out);
out.push_str(" (_ bv0 256)) (_ bv0 256) ((_ extract 255 0) (bvurem (");
out.push_str(op);
out.push_str(" ((_ zero_extend 256) ");
left.write_smt(cx, out);
out.push_str(") ((_ zero_extend 256) ");
right.write_smt(cx, out);
out.push_str(")) ((_ zero_extend 256) ");
modulus.write_smt(cx, out);
out.push_str("))))");
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum SymTernOp {
AddMod,
Expand Down
77 changes: 66 additions & 11 deletions crates/evm/symbolic/src/runtime/solver/smt/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Query-level SMT-LIB assertion emission with common-subexpression bindings.

use super::*;
use super::{normalize::ConstraintContext, *};

pub(super) fn write_smt_assertions(
cx: &SymCx,
Expand All @@ -14,14 +14,7 @@ pub(super) fn write_smt_assertions(
return Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"));
}

let plan = SmtCsePlan::new(constraints);
if plan.bindings.is_empty() {
for constraint in constraints {
let _ = writeln!(out, "(assert {})", constraint.smt(cx));
}
return Ok(());
}

let plan = SmtCsePlan::new(constraints, narrow_division_widths(constraints));
let writer = SmtCseWriter { cx, plan: &plan };
// define binding_0 = term_0
// ...
Expand Down Expand Up @@ -57,14 +50,16 @@ struct SmtCsePlan {
expr_visits: HashMap<SymExpr, SmtCseVisit>,
bool_visits: HashMap<SymBoolExpr, SmtCseVisit>,
bindings: Vec<SmtBinding>,
narrow_divisions: HashMap<SymExpr, usize>,
}

impl SmtCsePlan {
fn new(constraints: &[SymBoolExpr]) -> Self {
fn new(constraints: &[SymBoolExpr], narrow_divisions: HashMap<SymExpr, usize>) -> Self {
let mut plan = Self {
expr_visits: HashMap::default(),
bool_visits: HashMap::default(),
bindings: Vec::new(),
narrow_divisions,
};
for constraint in constraints {
plan.count_bool(constraint);
Expand All @@ -88,9 +83,13 @@ impl SmtCsePlan {
| SymExprKind::Keccak { .. }
| SymExprKind::Hash { .. } => {}
SymExprKind::Not(value) => self.count_expr(value),
SymExprKind::BinOp(_, left, right) => {
SymExprKind::BinOp(op, left, right) => {
self.count_expr(left);
self.count_expr(right);
// Narrow unsigned division emits the divisor again for its zero case.
if *op == SymBinOp::UDiv && self.narrow_divisions.contains_key(expr) {
self.count_expr(right);
}
}
SymExprKind::TernOp(_, left, right, modulus) => {
self.count_expr(modulus);
Expand Down Expand Up @@ -281,6 +280,10 @@ impl SmtCseWriter<'_> {
out.push(')');
}
SymExprKind::BinOp(op, left, right) => {
if let Some(width) = self.plan.narrow_divisions.get(expr) {
self.write_narrow_division(out, *op, left, right, *width);
return;
}
let _ = write!(out, "({} ", op.smt());
self.write_expr(out, left, skip_expr, skip_bool);
out.push(' ');
Expand All @@ -302,6 +305,39 @@ impl SmtCseWriter<'_> {
}
}

/// Writes a division whose operands fit `width` bits over that width.
///
/// `bvurem` by zero returns the dividend at any width. `bvudiv` by zero returns all ones, so
/// that case keeps the 256-bit result.
fn write_narrow_division(
&self,
out: &mut String,
op: SymBinOp,
left: &SymExpr,
right: &SymExpr,
width: usize,
) {
if op == SymBinOp::UDiv {
out.push_str("(ite (= ");
self.write_expr(out, right, None, None);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This emits right both in the zero check and again in the division, but SmtCsePlan counts uses before this duplication. After normalization removes provably nonzero EVM guards, single-use nested divisors therefore expand exponentially instead of receiving bindings. Repro: in a pure check with uint8 x, uint8 d, initialize assembly q := add(d, 1), unroll N steps of q := add(1, div(x, q)), finish with q := div(x, q), and assert q <= x. At N = 8/12/16, the reviewed head emits 122,929/1,966,129/31,457,329 bytes of SMT with no CSE definitions. Each extra level doubles the query, so small additional nesting leads to excessive memory use during query construction, before the solver timeout applies. Please bind the reused divisor or account for these synthesized uses in the CSE plan.

let _ = write!(out, " (_ bv0 256)) (_ bv{} 256) ", U256::MAX);
}
let _ = write!(
out,
"((_ zero_extend {}) ({} ((_ extract {} 0) ",
256 - width,
op.smt(),
width - 1
);
self.write_expr(out, left, None, None);
let _ = write!(out, ") ((_ extract {} 0) ", width - 1);
self.write_expr(out, right, None, None);
out.push_str(")))");
if op == SymBinOp::UDiv {
out.push(')');
}
}

fn write_wide_modular_arithmetic(
&self,
out: &mut String,
Expand Down Expand Up @@ -366,3 +402,22 @@ impl SmtCseWriter<'_> {
}
}
}

/// Returns the bit width each unsigned division can run at once the constraints bound both
/// operands, so the solver bit-blasts a narrow divider instead of a 256-bit one.
fn narrow_division_widths(constraints: &[SymBoolExpr]) -> HashMap<SymExpr, usize> {
let context = ConstraintContext::new(constraints);
let mut widths = HashMap::default();
for constraint in constraints {
let _ = constraint.visit_exprs(&mut |expr| {
if let SymExprKind::BinOp(SymBinOp::UDiv | SymBinOp::URem, left, right) = expr.kind() {
let width = context.unsigned_bits(left).max(context.unsigned_bits(right));
if width < 256 {
widths.insert(expr.clone(), width);
}
}
ControlFlow::<()>::Continue(())
});
}
widths
}
56 changes: 56 additions & 0 deletions crates/forge/tests/cli/test_cmd/symbolic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,16 @@ contract SymbolicSaturatingMul {
assert(SaturatingMath.saturatingMul(x, y) == 0);
}
}

// Division on narrow operands runs at their width in the solver.
function checkNarrowDivision(uint8 x, uint8 d) public pure {
require(d != 0);
uint256 z;
assembly {
z := div(x, d)
}
assert(z == x / d);
}
}
"#,
);
Expand All @@ -1160,6 +1170,16 @@ contract SymbolicSaturatingMul {
assert_eq!(result["symbolic"]["solver"]["stats"]["smt_queries"], 0);
assert_eq!(result["symbolic"]["solver"]["stats"]["heuristic_witnesses"], 0);

let output = cmd
.forge_fuse()
.args(["test", "--symbolic", "--json", "--match-test", "checkNarrowDivision"])
.assert_success()
.get_output()
.stdout
.clone();
let result = json_test_result(&output, "checkNarrowDivision(uint8,uint8)");
assert_eq!(result["symbolic"]["status"], "pass");

let output = cmd
.forge_fuse()
.args([
Expand All @@ -1179,6 +1199,42 @@ contract SymbolicSaturatingMul {
assert_eq!(result["symbolic"]["replay"]["status"], "confirmed");
}

#[forgetest_init]
fn symbolic_shares_nested_narrow_divisors(prj: _, cmd: _) {
skip_unless_z3!("symbolic_shares_nested_narrow_divisors");

let steps = "q := add(1, div(x, q))\n".repeat(12);
let source = format!(
r#"
contract SymbolicNestedDivisors {{
function checkNestedDivisors(uint8 x, uint8 d) public pure {{
uint256 q;
assembly {{
q := add(d, 1)
{steps}
q := div(x, q)
}}
assert(q <= x);
}}
}}
"#
);
prj.add_test("SymbolicNestedDivisors.t.sol", &source);

let output = cmd
.args(["test", "--symbolic", "--json", "--match-test", "checkNestedDivisors"])
.assert_success()
.get_output()
.stdout
.clone();
let result = json_test_result(&output, "checkNestedDivisors(uint8,uint8)");
assert_eq!(result["symbolic"]["status"], "pass", "{result}");
let max_query_bytes = result["symbolic"]["solver"]["stats"]["smt_max_query_bytes"]
.as_u64()
.expect("SMT query size");
assert!(max_query_bytes > 0 && max_query_bytes < 100_000, "{result}");
}

#[forgetest_init]
fn symbolic_proves_p256_normalization(prj: _, cmd: _) {
if !z3_available() {
Expand Down
Loading