Skip to content

Commit a12c58b

Browse files
Merge pull request #85 from mojtaba-eshghie/feature_issue_84
Feature #84: Created the witness generation functionality, its tests,…
2 parents 3cf390c + 8f3eb55 commit a12c58b

6 files changed

Lines changed: 680 additions & 23 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ __pycache__/
66
# C extensions
77
*.so
88

9+
temp_venv
10+
911
# Distribution / packaging
1012
.Python
1113
build/

src/sindi/cli.py

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# src/sindi/cli.py
12
#!/usr/bin/env python3
23
import argparse
34
import io
@@ -10,6 +11,7 @@
1011
from .parser import Parser, ASTNode
1112
from .simplifier import Simplifier
1213
from .comparator import Comparator
14+
from .witness import DomainSpec
1315
from .utils import printer, set_quiet, set_debug
1416
from .comparator_light import ComparatorRulesOnly
1517
import os
@@ -42,6 +44,8 @@ def read_predicate(value: str, is_file: bool) -> str:
4244
with open(value, "r", encoding="utf-8") as f:
4345
return f.read().strip()
4446

47+
# --- Existing Commands (Rewrite, Tokenize, Parse, Simplify) ---
48+
4549
def cmd_rewrite(args: argparse.Namespace) -> int:
4650
_configure_logging(args)
4751
rw = Rewriter()
@@ -91,9 +95,7 @@ def cmd_simplify(args: argparse.Namespace) -> int:
9195
tokens = tk.tokenize(s)
9296
ast = Parser(tokens).parse()
9397
simplified = sp.simplify(ast)
94-
9598
out: Dict[str, Any] = {"simplified_ast": ast_to_dict(simplified)}
96-
9799
if args.show_sympy:
98100
try:
99101
out["sympy_expr_original"] = str(sp._to_sympy(ast)) # type: ignore[attr-defined]
@@ -103,7 +105,6 @@ def cmd_simplify(args: argparse.Namespace) -> int:
103105
out["sympy_expr_simplified"] = str(sp._to_sympy(simplified)) # type: ignore[attr-defined]
104106
except Exception as e:
105107
out["sympy_expr_simplified_error"] = str(e)
106-
107108
if args.json:
108109
print(json.dumps(out, ensure_ascii=False))
109110
else:
@@ -120,31 +121,25 @@ def cmd_compare(args: argparse.Namespace) -> int:
120121
_configure_logging(args)
121122
rw = Rewriter()
122123
tk = Tokenizer()
123-
124124
p1 = read_predicate(args.predicate1, args.p1_file)
125125
p2 = read_predicate(args.predicate2, args.p2_file)
126-
127126
if args.light:
128127
cmp = ComparatorRulesOnly(verbose=args.verbose)
129128
else:
130129
cmp = Comparator()
131-
132130
sink = io.StringIO()
133131
with redirect_stdout(sink):
134132
verdict = cmp.compare(p1, p2)
135-
136133
if not args.verbose and not args.json:
137134
print(verdict)
138135
return 0
139-
140136
out: Dict[str, Any] = {"verdict": verdict}
141137
rp1 = rw.apply(p1)
142138
rp2 = rw.apply(p2)
143139
out["rewritten"] = {"p1": rp1, "p2": rp2}
144140
ast1 = Parser(tk.tokenize(rp1)).parse()
145141
ast2 = Parser(tk.tokenize(rp2)).parse()
146142
out["ast"] = {"p1": ast_to_dict(ast1), "p2": ast_to_dict(ast2)}
147-
148143
if args.json:
149144
print(json.dumps(out, ensure_ascii=False))
150145
else:
@@ -156,55 +151,143 @@ def cmd_compare(args: argparse.Namespace) -> int:
156151
print_tree(ast2)
157152
return 0
158153

154+
# --- NEW: Witness Command ---
155+
156+
def parse_domains(domain_str: str) -> Dict[str, DomainSpec]:
157+
"""Parses 'val:uint8,flag:bool' into domain dict."""
158+
if not domain_str:
159+
return {}
160+
domains = {}
161+
for part in domain_str.split(','):
162+
if ':' not in part:
163+
continue
164+
var, spec = part.split(':', 1)
165+
var = var.strip()
166+
spec = spec.strip().lower()
167+
168+
if spec == 'bool':
169+
domains[var] = DomainSpec(kind='bool')
170+
elif spec.startswith('uint'):
171+
# handle uint, uint8, uint256
172+
bits = 256
173+
if len(spec) > 4:
174+
try: bits = int(spec[4:])
175+
except: pass
176+
domains[var] = DomainSpec(kind='uint', bits=bits)
177+
elif spec.startswith('int'):
178+
bits = 256
179+
if len(spec) > 3:
180+
try: bits = int(spec[3:])
181+
except: pass
182+
domains[var] = DomainSpec(kind='int', bits=bits)
183+
elif spec == 'address':
184+
domains[var] = DomainSpec(kind='address')
185+
return domains
186+
187+
def cmd_witness(args: argparse.Namespace) -> int:
188+
_configure_logging(args)
189+
190+
# Read Predicates
191+
new_pred = read_predicate(args.new_pred, args.new_file)
192+
old_pred = read_predicate(args.old_pred, args.old_file)
193+
194+
# Parse Domains
195+
domains = parse_domains(args.domains) if args.domains else None
196+
197+
# Solve
198+
comp = Comparator()
199+
res = comp.witness_solve(new_pred, old_pred, domains=domains)
200+
201+
# Output
202+
out = {
203+
"sat": res.sat,
204+
"model": res.model,
205+
"error": res.error,
206+
"unconstrained": res.unconstrained
207+
}
208+
209+
if args.verbose:
210+
out["z3_formula"] = res.z3_formula
211+
out["simplified_new"] = res.simplified_new
212+
out["simplified_old"] = res.simplified_old
213+
214+
if args.json:
215+
print(json.dumps(out, indent=2, ensure_ascii=False))
216+
else:
217+
if res.sat is True:
218+
print("Status: SAT (Weakening Found)")
219+
print("Witness Model:")
220+
for k, v in res.model.items():
221+
print(f" {k} = {v}")
222+
elif res.sat is False:
223+
print("Status: UNSAT (No weakening found / Equivalent)")
224+
else:
225+
print(f"Status: ERROR/UNKNOWN ({res.error})")
226+
227+
return 0 if res.sat is not None else 1
228+
159229
def build_parser() -> argparse.ArgumentParser:
160230
p = argparse.ArgumentParser(
161231
prog="sindi",
162-
description="SInDi CLI: rewrite, tokenize, parse, simplify, and compare Solidity predicates."
232+
description="SInDi CLI: rewrite, tokenize, parse, simplify, compare, and generate witnesses for Solidity predicates."
163233
)
164234
sub = p.add_subparsers(dest="cmd", required=True)
165-
235+
236+
# Rewrite
166237
pr = sub.add_parser("rewrite", help="Apply rewrite rules and print the result.")
167238
pr.add_argument("predicate")
168239
pr.add_argument("--from-file", action="store_true")
169240
pr.set_defaults(func=cmd_rewrite)
170-
241+
242+
# Tokenize
171243
pt = sub.add_parser("tokenize", help="Tokenize (optionally after rewrite).")
172244
pt.add_argument("predicate")
173245
pt.add_argument("--from-file", action="store_true")
174246
pt.add_argument("--skip-rewrite", action="store_true")
175247
pt.add_argument("--json", action="store_true")
176248
pt.set_defaults(func=cmd_tokenize)
177-
249+
250+
# Parse
178251
pp = sub.add_parser("parse", help="Parse into AST (optionally after rewrite).")
179252
pp.add_argument("predicate")
180253
pp.add_argument("--from-file", action="store_true")
181254
pp.add_argument("--skip-rewrite", action="store_true")
182255
pp.add_argument("--tree", action="store_true")
183256
pp.add_argument("--json", action="store_true")
184257
pp.set_defaults(func=cmd_parse)
185-
258+
259+
# Simplify
186260
ps = sub.add_parser("simplify", help="Simplify AST (SymPy-backed).")
187261
ps.add_argument("predicate")
188262
ps.add_argument("--from-file", action="store_true")
189263
ps.add_argument("--skip-rewrite", action="store_true")
190264
ps.add_argument("--show-sympy", action="store_true")
191265
ps.add_argument("--json", action="store_true")
192266
ps.set_defaults(func=cmd_simplify)
193-
267+
268+
# Compare
194269
pc = sub.add_parser("compare", help="Compare two predicates and print verdict.")
195270
pc.add_argument("predicate1")
196271
pc.add_argument("predicate2")
197272
pc.add_argument("--p1-file", action="store_true")
198273
pc.add_argument("--p2-file", action="store_true")
199-
pc.add_argument("--light", action="store_true",
200-
help="Use solver-free ComparatorRulesOnly.")
201-
pc.add_argument("--verbose", action="store_true",
202-
help="Show rewritten predicates and ASTs.")
274+
pc.add_argument("--light", action="store_true", help="Use solver-free ComparatorRulesOnly.")
275+
pc.add_argument("--verbose", action="store_true", help="Show rewritten predicates and ASTs.")
203276
pc.add_argument("--json", action="store_true")
204-
pc.add_argument("--debug-logs", action="store_true",
205-
help="Do not silence internal debug prints.")
277+
pc.add_argument("--debug-logs", action="store_true", help="Do not silence internal debug prints.")
206278
pc.set_defaults(func=cmd_compare)
207279

280+
# Witness (NEW)
281+
pw = sub.add_parser("witness", help="Find a variable assignment satisfying New AND NOT Old.")
282+
pw.add_argument("new_pred", help="The new (weaker?) predicate")
283+
pw.add_argument("old_pred", help="The old (stronger?) predicate")
284+
pw.add_argument("--new-file", action="store_true", help="Read new_pred from file")
285+
pw.add_argument("--old-file", action="store_true", help="Read old_pred from file")
286+
pw.add_argument("--domains", help="Comma-separated domains, e.g. 'x:uint8,flag:bool'")
287+
pw.add_argument("--json", action="store_true", help="Output JSON result")
288+
pw.add_argument("--verbose", action="store_true", help="Include debug formulas in JSON")
289+
pw.set_defaults(func=cmd_witness)
290+
208291
return p
209292

210293
def main() -> int:
@@ -220,4 +303,4 @@ def main() -> int:
220303
return 1
221304

222305
if __name__ == "__main__":
223-
sys.exit(main())
306+
sys.exit(main())

src/sindi/comparator.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .rewriter import Rewriter
88
from .utils import printer
99
from .ast_rewriter import ASTRewriter
10+
from .witness import WitnessSolver
1011
import z3
1112
import re
1213

@@ -595,4 +596,20 @@ def _has_numeric_scale(e) -> bool:
595596
except Exception as e:
596597
printer(f"Error (satisfiability error): {e}", level)
597598
return False
598-
return False
599+
return False
600+
601+
def witness_solve(
602+
self,
603+
new_pred: str,
604+
old_pred: str,
605+
*,
606+
domains: dict | None = None,
607+
simplify: bool = True
608+
):
609+
"""
610+
Attempts to find a witness (input assignment) where new_pred is True
611+
and old_pred is False.
612+
Returns a WitnessResult object.
613+
"""
614+
solver = WitnessSolver(self)
615+
return solver.solve(new_pred, old_pred, domains=domains, simplify=simplify)

0 commit comments

Comments
 (0)