use crate::parser::contracts::{Contract, ContractType};
#[cfg(feature = "z3")]
use crate::translator::z3_translator::{
returns_bool_like, returns_result_or_option_result, Z3Translator, Z3VarMap,
};
#[cfg(feature = "z3")]
use z3::ast::{forall_const, Ast, Bool, Int};
#[cfg(feature = "z3")]
use z3::{Context, SatResult, Solver, Sort};
#[cfg(feature = "z3")]
#[derive(Debug, Clone)]
pub enum VerificationResult {
Verified,
Failed {
counterexample: Option<Counterexample>,
},
Unknown { reason: String },
Error { error: String },
}
#[cfg(feature = "z3")]
#[derive(Debug, Clone)]
pub struct Counterexample {
pub assignments: std::collections::HashMap<String, String>,
}
#[cfg(feature = "z3")]
pub struct Z3Verifier {
translator: Z3Translator,
}
#[cfg(feature = "z3")]
impl Z3Verifier {
pub fn new(timeout_ms: u64) -> Self {
let translator = Z3Translator::new(timeout_ms);
Z3Verifier { translator }
}
pub fn verify_contract(&mut self, contract: &Contract) -> VerificationResult {
self.verify_contract_with_context(contract, None, &[])
}
pub fn verify_contract_with_context(
&mut self,
contract: &Contract,
func_sig: Option<&syn::ItemFn>,
requires_contracts: &[Contract],
) -> VerificationResult {
let (param_types, return_type) = if let Some(func) = func_sig {
(extract_parameter_types(func), extract_return_type(func))
} else {
(std::collections::HashMap::new(), None)
};
let (z3_expr, type_constraints) = match self.translator.translate_contract_with_types(
contract,
¶m_types,
return_type.as_ref(),
) {
Ok((expr, constraints)) => (expr, constraints),
Err(e) => {
return VerificationResult::Error {
error: format!("Translation error: {e}"),
};
}
};
let negated_bool = match z3_expr.as_bool() {
Some(b) => b.not(),
None => {
return VerificationResult::Error {
error: "Contract expression must be boolean".to_string(),
};
}
};
let ctx = self.translator.context();
let mut solver = Solver::new(ctx);
for constraint in &type_constraints {
solver.assert(constraint);
}
let mut body_translated = false;
if matches!(contract.contract_type, ContractType::Ensures) {
for requires_contract in requires_contracts {
if let Ok((requires_expr, requires_constraints)) =
self.translator.translate_contract_with_types(
requires_contract,
¶m_types,
return_type.as_ref(),
)
{
for constraint in &requires_constraints {
solver.assert(constraint);
}
if let Some(requires_bool) = requires_expr.as_bool() {
solver.assert(&requires_bool);
}
}
}
if let Some(func) = func_sig {
let mut body_vars = Z3VarMap::new();
for name in param_types.keys() {
let symbol = z3::Symbol::String(name.clone());
let var = z3::ast::Int::new_const(ctx, symbol);
body_vars.insert(name.clone(), var.into());
}
let result_symbol = z3::Symbol::String("result".to_string());
let result_var = if return_type.as_ref().is_some_and(returns_bool_like) {
z3::ast::Bool::new_const(ctx, result_symbol).into()
} else {
z3::ast::Int::new_const(ctx, result_symbol).into()
};
body_vars.insert("result".to_string(), result_var);
add_shift_axioms(ctx, &mut solver);
if let Ok(Some(impl_formula)) = self
.translator
.translate_function_body(func, &mut body_vars)
{
solver.assert(&impl_formula);
body_translated = true;
}
}
}
solver.assert(&negated_bool);
match solver.check() {
SatResult::Unsat => {
VerificationResult::Verified
}
SatResult::Sat => {
if !body_translated {
return VerificationResult::Unknown {
reason: "Could not translate function body to Z3 constraints; \
SAT result without body constraints is not meaningful"
.to_string(),
};
}
let counterexample = self.extract_counterexample(&solver);
VerificationResult::Failed { counterexample }
}
SatResult::Unknown => VerificationResult::Unknown {
reason: "Z3 solver returned Unknown (timeout or complexity). Try --timeout 30."
.to_string(),
},
}
}
pub fn check_ensures_formula_sat_smoke(&mut self, contract: &Contract) -> VerificationResult {
if !matches!(contract.contract_type, ContractType::Ensures) {
return VerificationResult::Error {
error: "check_ensures_formula_sat_smoke expects an Ensures contract".to_string(),
};
}
let (z3_expr, type_constraints) = match self.translator.translate_contract_with_types(
contract,
&std::collections::HashMap::new(),
None,
) {
Ok(x) => x,
Err(e) => {
return VerificationResult::Error {
error: format!("Translation error: {e}"),
};
}
};
let pos = match z3_expr.as_bool() {
Some(b) => b,
None => {
return VerificationResult::Error {
error: "Formula must translate to a boolean Z3 expression".to_string(),
};
}
};
let ctx = self.translator.context();
let mut solver = Solver::new(ctx);
add_shift_axioms(ctx, &mut solver);
for c in &type_constraints {
solver.assert(c);
}
solver.assert(&pos);
match solver.check() {
SatResult::Sat => VerificationResult::Verified,
SatResult::Unsat => VerificationResult::Failed {
counterexample: None,
},
SatResult::Unknown => VerificationResult::Unknown {
reason: "Z3 Unknown (formula satisfiability smoke)".to_string(),
},
}
}
fn extract_counterexample(&self, solver: &Solver<'_>) -> Option<Counterexample> {
let _model = solver.get_model()?;
Some(Counterexample {
assignments: std::collections::HashMap::new(),
})
}
pub fn reset(&mut self) {
}
pub fn verify_determinism(
&mut self,
func: &syn::ItemFn,
requires_contracts: &[Contract],
) -> VerificationResult {
let ctx = self.translator.context();
let param_types = extract_parameter_types(func);
let return_type = extract_return_type(func);
if param_types.is_empty() {
return VerificationResult::Verified;
}
let mut solver = Solver::new(ctx);
add_shift_axioms(ctx, &mut solver);
let prefix1 = "r1_";
let prefix2 = "r2_";
let mut vars1 = Z3VarMap::new();
let mut vars2 = Z3VarMap::new();
for name in param_types.keys() {
let sym1 = z3::Symbol::String(format!("{prefix1}{name}"));
let sym2 = z3::Symbol::String(format!("{prefix2}{name}"));
let var1 = z3::ast::Int::new_const(ctx, sym1);
let var2 = z3::ast::Int::new_const(ctx, sym2);
vars1.insert(name.clone(), var1.into());
vars2.insert(name.clone(), var2.into());
}
let result_sym1 = z3::Symbol::String(format!("{prefix1}result"));
let result_sym2 = z3::Symbol::String(format!("{prefix2}result"));
let func_name = func.sig.ident.to_string();
let use_int_result = return_type
.as_ref()
.is_some_and(returns_result_or_option_result)
&& matches!(
func_name.as_str(),
"verify_script_with_context_full"
| "verify_script_with_context"
| "try_verify_p2pk_fast_path"
);
let result1: z3::ast::Dynamic = if use_int_result {
z3::ast::Int::new_const(ctx, result_sym1).into()
} else if return_type.as_ref().is_some_and(returns_bool_like) {
z3::ast::Bool::new_const(ctx, result_sym1).into()
} else {
z3::ast::Int::new_const(ctx, result_sym1).into()
};
let result2: z3::ast::Dynamic = if use_int_result {
z3::ast::Int::new_const(ctx, result_sym2).into()
} else if return_type.as_ref().is_some_and(returns_bool_like) {
z3::ast::Bool::new_const(ctx, result_sym2).into()
} else {
z3::ast::Int::new_const(ctx, result_sym2).into()
};
vars1.insert("result".to_string(), result1.clone());
vars2.insert("result".to_string(), result2.clone());
let formula1 = match self.translator.translate_function_body(func, &mut vars1) {
Ok(Some(f)) => f,
Ok(None) => {
return VerificationResult::Error {
error: "Could not translate body for run 1 (no formula)".to_string(),
}
}
Err(e) => {
return VerificationResult::Error {
error: format!("Could not translate body for run 1: {e}"),
}
}
};
let formula2 = match self.translator.translate_function_body(func, &mut vars2) {
Ok(Some(f)) => f,
Ok(None) => {
return VerificationResult::Error {
error: "Could not translate body for run 2 (no formula)".to_string(),
}
}
Err(e) => {
return VerificationResult::Error {
error: format!("Could not translate body for run 2: {e}"),
}
}
};
solver.assert(&formula1);
solver.assert(&formula2);
for req in requires_contracts {
if let Ok(expr1) = self
.translator
.translate_expr_with_vars(&req.condition, &mut vars1)
{
if let Some(b1) = expr1.as_bool() {
solver.assert(&b1);
}
}
if let Ok(expr2) = self
.translator
.translate_expr_with_vars(&req.condition, &mut vars2)
{
if let Some(b2) = expr2.as_bool() {
solver.assert(&b2);
}
}
}
let mut input_equiv = Vec::new();
for name in param_types.keys() {
let v1 = vars1.get(name).and_then(|x| x.as_int()).unwrap();
let v2 = vars2.get(name).and_then(|x| x.as_int()).unwrap();
input_equiv.push(v1._eq(&v2));
}
let equiv_refs: Vec<&Bool> = input_equiv.iter().collect();
let input_equal = Bool::and(ctx, &equiv_refs);
let output_diff = if let (Some(r1), Some(r2)) = (result1.as_int(), result2.as_int()) {
r1._eq(&r2).not()
} else if let (Some(r1), Some(r2)) = (result1.as_bool(), result2.as_bool()) {
r1._eq(&r2).not()
} else {
return VerificationResult::Error {
error: "Result type not Int or Bool".to_string(),
};
};
let negated = Bool::and(ctx, &[&input_equal, &output_diff]);
solver.assert(&negated);
match solver.check() {
SatResult::Unsat => VerificationResult::Verified,
SatResult::Sat => VerificationResult::Failed {
counterexample: self.extract_counterexample(&solver),
},
SatResult::Unknown => VerificationResult::Unknown {
reason: "Z3 returned Unknown (determinism check). Try --timeout 30.".to_string(),
},
}
}
}
#[cfg(all(test, feature = "z3"))]
mod formula_sat_smoke_tests {
use super::{VerificationResult, Z3Verifier};
use crate::parser::contracts::{Contract, ContractType};
use syn::parse_str;
#[test]
fn ensures_literal_true_is_sat() {
let mut v = Z3Verifier::new(5000);
let c = Contract {
contract_type: ContractType::Ensures,
condition: parse_str("true").unwrap(),
comment: None,
};
assert!(matches!(
v.check_ensures_formula_sat_smoke(&c),
VerificationResult::Verified
));
}
#[test]
fn result_eq_result_is_sat() {
let mut v = Z3Verifier::new(5000);
let c = Contract {
contract_type: ContractType::Ensures,
condition: parse_str("result == result").unwrap(),
comment: None,
};
assert!(matches!(
v.check_ensures_formula_sat_smoke(&c),
VerificationResult::Verified
));
}
#[test]
fn x_lt_x_is_unsat_contradiction() {
let mut v = Z3Verifier::new(5000);
let c = Contract {
contract_type: ContractType::Ensures,
condition: parse_str("x < x").unwrap(),
comment: None,
};
assert!(matches!(
v.check_ensures_formula_sat_smoke(&c),
VerificationResult::Failed { .. }
));
}
}
#[cfg(feature = "z3")]
fn add_shift_axioms(ctx: &Context, solver: &mut Solver) {
let int_sort = Sort::int(ctx);
let a = Int::new_const(ctx, "axiom_a");
let b = Int::new_const(ctx, "axiom_b");
let zero = Int::from_i64(ctx, 0);
let shr_fn = z3::FuncDecl::new(ctx, "shr", &[&int_sort, &int_sort], &int_sort);
let shl_fn = z3::FuncDecl::new(ctx, "shl", &[&int_sort, &int_sort], &int_sort);
let shr_result = shr_fn.apply(&[&a, &b]);
let shr_result_int = shr_result.as_int().unwrap();
let premise1 = Bool::and(ctx, &[&a.ge(&zero), &b.ge(&zero)]);
let conclusion1 = shr_result_int.ge(&zero);
let axiom1 = premise1.implies(&conclusion1);
let bound_a = a.clone();
let bound_b = b.clone();
let forall1 = forall_const(ctx, &[&bound_a, &bound_b], &[], &axiom1);
solver.assert(&forall1);
let conclusion2 = shr_result_int.le(&a);
let axiom2 = premise1.implies(&conclusion2);
let forall2 = forall_const(ctx, &[&bound_a, &bound_b], &[], &axiom2);
solver.assert(&forall2);
let shr_by_zero = shr_fn.apply(&[&a, &zero]);
let shr_by_zero_int = shr_by_zero.as_int().unwrap();
let axiom3 = shr_by_zero_int._eq(&a);
let forall3 = forall_const(ctx, &[&bound_a], &[], &axiom3);
solver.assert(&forall3);
let shl_result = shl_fn.apply(&[&a, &b]);
let shl_result_int = shl_result.as_int().unwrap();
let conclusion4 = shl_result_int.ge(&a);
let axiom4 = premise1.implies(&conclusion4);
let forall4 = forall_const(ctx, &[&bound_a, &bound_b], &[], &axiom4);
solver.assert(&forall4);
let shl_by_zero = shl_fn.apply(&[&a, &zero]);
let shl_by_zero_int = shl_by_zero.as_int().unwrap();
let axiom5 = shl_by_zero_int._eq(&a);
let forall5 = forall_const(ctx, &[&bound_a], &[], &axiom5);
solver.assert(&forall5);
for k in 0_u32..64 {
let k_const = Int::from_i64(ctx, i64::from(k));
let two_pow_k = Z3Translator::pow2_int(ctx, k);
let shr_ak = shr_fn.apply(&[&a, &k_const]);
let shr_ak_int = shr_ak.as_int().unwrap();
let div_ak = a.clone().div(&two_pow_k);
let axiom = shr_ak_int._eq(&div_ak);
let forall_k = forall_const(ctx, &[&bound_a], &[], &axiom);
solver.assert(&forall_k);
}
}
fn extract_parameter_types(func: &syn::ItemFn) -> std::collections::HashMap<String, syn::Type> {
let mut types = std::collections::HashMap::new();
for input in &func.sig.inputs {
if let syn::FnArg::Typed(pat_type) = input {
if let syn::Pat::Ident(ident) = &*pat_type.pat {
types.insert(ident.ident.to_string(), *pat_type.ty.clone());
}
}
}
types
}
fn extract_return_type(func: &syn::ItemFn) -> Option<syn::Type> {
if let syn::ReturnType::Type(_, ty) = &func.sig.output {
Some(*ty.clone())
} else {
None
}
}
#[cfg(not(feature = "z3"))]
pub struct Z3Verifier;
#[cfg(not(feature = "z3"))]
impl Z3Verifier {
pub fn new(_timeout_ms: u64) -> Self {
Z3Verifier
}
pub fn verify_contract(&mut self, _contract: &Contract) -> VerificationResult {
VerificationResult::Error {
error: "Z3 feature not enabled".to_string(),
}
}
pub fn check_ensures_formula_sat_smoke(&mut self, _contract: &Contract) -> VerificationResult {
VerificationResult::Error {
error: "Z3 feature not enabled".to_string(),
}
}
}
#[cfg(not(feature = "z3"))]
#[derive(Debug, Clone)]
pub enum VerificationResult {
Error { error: String },
}