use std::collections::BTreeMap;
use crate::ast::*;
use crate::span::Spanned;
pub const GOLDILOCKS_P: u64 = crate::field::goldilocks::MODULUS;
mod executor;
mod expr;
#[cfg(test)]
mod tests;
pub use executor::*;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum SymValue {
Const(u64),
Var(SymVar),
Add(Box<SymValue>, Box<SymValue>),
Mul(Box<SymValue>, Box<SymValue>),
Sub(Box<SymValue>, Box<SymValue>),
Neg(Box<SymValue>),
Inv(Box<SymValue>),
Eq(Box<SymValue>, Box<SymValue>),
Lt(Box<SymValue>, Box<SymValue>),
Hash(Vec<SymValue>, usize),
Divine(u32),
FieldAccess(Box<SymValue>, String),
PubInput(u32),
Ite(Box<SymValue>, Box<SymValue>, Box<SymValue>),
}
impl SymValue {
pub fn is_const(&self) -> bool {
matches!(self, SymValue::Const(_))
}
pub fn as_const(&self) -> Option<u64> {
match self {
SymValue::Const(v) => Some(*v),
_ => None,
}
}
pub fn contains_opaque(&self) -> bool {
match self {
SymValue::Hash(_, _) => true,
SymValue::Var(var) => {
var.name.starts_with("__proj_")
|| var.name.starts_with("__hash")
|| var.name.starts_with("__divine")
}
SymValue::Add(a, b)
| SymValue::Mul(a, b)
| SymValue::Sub(a, b)
| SymValue::Eq(a, b)
| SymValue::Lt(a, b) => a.contains_opaque() || b.contains_opaque(),
SymValue::Neg(a) | SymValue::Inv(a) => a.contains_opaque(),
SymValue::Ite(c, t, e) => {
c.contains_opaque() || t.contains_opaque() || e.contains_opaque()
}
SymValue::FieldAccess(inner, _) => inner.contains_opaque(),
SymValue::Const(_) | SymValue::Divine(_) | SymValue::PubInput(_) => false,
}
}
pub fn is_external_input(&self) -> bool {
match self {
SymValue::Var(var) => {
var.name.starts_with("pub_in_") || var.name.starts_with("divine_")
}
SymValue::PubInput(_) | SymValue::Divine(_) => true,
_ => false,
}
}
pub fn simplify(&self) -> SymValue {
match self {
SymValue::Add(a, b) => {
let a = a.simplify();
let b = b.simplify();
match (&a, &b) {
(SymValue::Const(0), _) => b,
(_, SymValue::Const(0)) => a,
(SymValue::Const(x), SymValue::Const(y)) => {
SymValue::Const(((*x as u128 + *y as u128) % GOLDILOCKS_P as u128) as u64)
}
_ => SymValue::Add(Box::new(a), Box::new(b)),
}
}
SymValue::Mul(a, b) => {
let a = a.simplify();
let b = b.simplify();
match (&a, &b) {
(SymValue::Const(0), _) | (_, SymValue::Const(0)) => SymValue::Const(0),
(SymValue::Const(1), _) => b,
(_, SymValue::Const(1)) => a,
(SymValue::Const(x), SymValue::Const(y)) => {
SymValue::Const(((*x as u128 * *y as u128) % GOLDILOCKS_P as u128) as u64)
}
_ => SymValue::Mul(Box::new(a), Box::new(b)),
}
}
SymValue::Sub(a, b) => {
let a = a.simplify();
let b = b.simplify();
match (&a, &b) {
(_, SymValue::Const(0)) => a,
(SymValue::Const(x), SymValue::Const(y)) => SymValue::Const(
(((*x as u128 + GOLDILOCKS_P as u128) - *y as u128) % GOLDILOCKS_P as u128)
as u64,
),
_ if a == b => SymValue::Const(0),
_ => SymValue::Sub(Box::new(a), Box::new(b)),
}
}
SymValue::Neg(a) => {
let a = a.simplify();
match &a {
SymValue::Const(0) => SymValue::Const(0),
SymValue::Const(v) => SymValue::Const(GOLDILOCKS_P - v),
_ => SymValue::Neg(Box::new(a)),
}
}
SymValue::Eq(a, b) => {
let a = a.simplify();
let b = b.simplify();
if a == b {
SymValue::Const(1)
} else {
match (&a, &b) {
(SymValue::Const(x), SymValue::Const(y)) => {
SymValue::Const(if x == y { 1 } else { 0 })
}
_ => SymValue::Eq(Box::new(a), Box::new(b)),
}
}
}
_ => self.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SymVar {
pub name: String,
pub version: u32,
}
impl std::fmt::Display for SymVar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.version == 0 {
write!(f, "{}", self.name)
} else {
write!(f, "{}_{}", self.name, self.version)
}
}
}
#[derive(Clone, Debug)]
pub enum Constraint {
Equal(SymValue, SymValue),
AssertTrue(SymValue),
Conditional(SymValue, Box<Constraint>),
RangeU32(SymValue),
DigestEqual(Vec<SymValue>, Vec<SymValue>),
}
impl Constraint {
pub fn is_trivial(&self) -> bool {
match self {
Constraint::Equal(a, b) => a == b,
Constraint::AssertTrue(v) => matches!(v, SymValue::Const(1)),
Constraint::RangeU32(v) => {
if let SymValue::Const(c) = v {
*c <= u32::MAX as u64
} else {
false
}
}
Constraint::DigestEqual(a, b) => a == b,
Constraint::Conditional(cond, inner) => {
matches!(cond, SymValue::Const(0)) || inner.is_trivial()
}
}
}
pub fn is_violated(&self) -> bool {
match self {
Constraint::Equal(SymValue::Const(a), SymValue::Const(b)) => a != b,
Constraint::AssertTrue(SymValue::Const(0)) => true,
Constraint::RangeU32(SymValue::Const(c)) => *c > u32::MAX as u64,
_ => false,
}
}
pub fn is_hash_dependent(&self) -> bool {
match self {
Constraint::Equal(a, b) => a.contains_opaque() || b.contains_opaque(),
Constraint::AssertTrue(v) => v.contains_opaque(),
Constraint::Conditional(_, inner) => inner.is_hash_dependent(),
Constraint::DigestEqual(a, b) => {
a.iter().any(|v| v.contains_opaque()) || b.iter().any(|v| v.contains_opaque())
}
Constraint::RangeU32(_) => true,
}
}
}
#[derive(Clone, Debug)]
pub struct ConstraintSystem {
pub constraints: Vec<Constraint>,
pub variables: BTreeMap<String, u32>,
pub pub_inputs: Vec<SymVar>,
pub pub_outputs: Vec<SymValue>,
pub divine_inputs: Vec<SymVar>,
pub num_variables: u32,
}
impl ConstraintSystem {
pub fn new() -> Self {
Self {
constraints: Vec::new(),
variables: BTreeMap::new(),
pub_inputs: Vec::new(),
pub_outputs: Vec::new(),
divine_inputs: Vec::new(),
num_variables: 0,
}
}
pub fn active_constraints(&self) -> usize {
self.constraints.iter().filter(|c| !c.is_trivial()).count()
}
pub fn violated_constraints(&self) -> Vec<&Constraint> {
self.constraints
.iter()
.filter(|c| c.is_violated())
.collect()
}
pub fn summary(&self) -> String {
format!(
"Variables: {}, Constraints: {} ({} active), Inputs: {} pub + {} divine, Outputs: {}",
self.num_variables,
self.constraints.len(),
self.active_constraints(),
self.pub_inputs.len(),
self.divine_inputs.len(),
self.pub_outputs.len(),
)
}
}
pub fn analyze(file: &File) -> ConstraintSystem {
SymExecutor::new().execute_file(file)
}
pub fn analyze_function(file: &File, fn_name: &str) -> ConstraintSystem {
SymExecutor::new().execute_function(file, fn_name)
}
pub fn analyze_all(file: &File) -> Vec<(String, ConstraintSystem)> {
let mut results = Vec::new();
for item in &file.items {
if let Item::Fn(func) = &item.node {
if func.body.is_some() && !func.is_test && func.intrinsic.is_none() {
let system = SymExecutor::new().execute_function(file, &func.name.node);
results.push((func.name.node.clone(), system));
}
}
}
results
}
#[derive(Clone, Debug)]
pub struct VerificationResult {
pub name: String,
pub total_constraints: usize,
pub active_constraints: usize,
pub violated: Vec<String>,
pub redundant_count: usize,
pub system_summary: String,
}
impl VerificationResult {
pub fn is_safe(&self) -> bool {
self.violated.is_empty()
}
pub fn format_report(&self) -> String {
let mut report = String::new();
report.push_str(&format!("Verification: {}\n", self.name));
report.push_str(&format!(" {}\n", self.system_summary));
report.push_str(&format!(
" Constraints: {} total, {} active, {} redundant\n",
self.total_constraints, self.active_constraints, self.redundant_count,
));
if self.violated.is_empty() {
report.push_str(" Status: SAFE (no trivially violated assertions)\n");
} else {
report.push_str(&format!(
" Status: VIOLATED ({} assertion(s) always fail)\n",
self.violated.len()
));
for v in &self.violated {
report.push_str(&format!(" - {}\n", v));
}
}
report
}
}
pub fn verify_file(file: &File) -> VerificationResult {
let system = analyze(file);
let violated: Vec<String> = system
.violated_constraints()
.iter()
.map(|c| format!("{:?}", c))
.collect();
let redundant_count = system.constraints.iter().filter(|c| c.is_trivial()).count();
VerificationResult {
name: file.name.node.clone(),
total_constraints: system.constraints.len(),
active_constraints: system.active_constraints(),
violated,
redundant_count,
system_summary: system.summary(),
}
}