equiv_receipt
equiv-receipt — portable, independently re-derivable logic-equivalence receipts.
Raised when a receipt fails verification.
Build a miter, refute it, and return an EQUIV-1 receipt.
UNSAT of the miter means the circuits are equivalent; a satisfying assignment is a concrete input on which they differ, and is bound into the receipt.
refute defaults to the bundled demonstration solver. Pass
equiv_receipt.solver.refute() to use an external one — the receipt is
unchanged in kind, because the solver was never trusted: its proof is
re-checked here and again by every reader.
Assemble an EQUIV-1 receipt.
verdict is "EQUIVALENT" or "COUNTEREXAMPLE". The verdict is
recorded but re-derived by verify_receipt(); recording it is a
convenience, never a basis for acceptance.
Re-derive the verdict and the commitments. Returns a result dict.
Checks, in order: format; chain integrity; commitment root over the payload; and — the one that matters — the verdict, re-derived by running the RUP checker over the committed formula and proof, or by re-simulating the committed counterexample.
Verify a DRAT refutation by forward RUP checking.
Returns a dict with verified, n_lemmas, and — on failure — the
failed_lemma and its zero-based failed_index so the defect is
locatable rather than merely reported.
A refutation is accepted when an empty clause is derived, or when the final active set propagates to a conflict with no assumptions.
Parse DRAT into ("a"|"d", clause) steps. Comment lines (c) are skipped.
Raises MalformedProof on unparseable input. Callers that must not
crash should use forward_rup_check(), which converts this into a
rejection — an unreadable proof proves nothing, which is a verdict, not an
error condition.
Unit propagation. Returns (conflict, assignment).
conflict is True when propagation derives a contradiction.
A clause store with two watched literals per clause.
The naive checker re-scans every active clause on every propagation round, and the active set grows by one on every lemma — so the cost of checking a proof grows worse than the product of its two dimensions. Watching two literals per clause means a clause is only visited when one of the two literals it is watching becomes false, which is what makes long proofs checkable.
The watch invariant survives between calls because every call propagates from an empty assignment and undoes nothing: there is no backtracking to invalidate.
Mark one clause with exactly these literals deleted. False if absent.
Deletion is lazy: the index entry is dropped and the clause is added to a deleted set that propagation skips. Removing it from its two watch lists eagerly would cost more than skipping it.
Unit propagation from assumed. Returns (conflict, assignment).
Semantics are identical to bcp(); only the cost differs.
Truth is tracked as a set of true literals rather than a variable-to-bool
map. "Is this literal true" becomes one set lookup and "is it false"
becomes one on its negation, which removes an abs() and a second
lookup from the innermost loop — measurably the hottest path in the file.
Search for a refutation, emitting DRAT lemmas.
Returns {"unsat": bool, "drat": str, "n_lemmas": int, "model": dict|None}.
When unsat is False a satisfying assignment is returned instead — for a
miter that is a counterexample: the two circuits differ on it.
Build a miter CNF for two single-output circuit builders.
build_a(net, prefix) -> output_signal_name. The returned CNF is
satisfiable iff the two circuits differ on some input assignment; therefore
UNSAT means equivalent.
A tiny combinational netlist over AND / OR / NOT / XOR gates.
Inputs are named; every gate produces a new named signal. Deliberately minimal — this exists so the package is usable and testable without a synthesis tool, not to replace one.
An alias. Needed when a signal must be renamed rather than computed.
Parse DIMACS CNF.
With strict=True (the default) a malformed header, a non-integer token, or
an unterminated final clause raises CNFParseError. Silently
returning a partial or empty clause list would let a caller reason confidently
about a formula that is not the one on disk — the worst failure mode available
to a proof checker.
Pass strict=False for the older lenient behaviour.
Serialize to DIMACS with a correct header.
The byte-level output is deterministic: this is what a receipt commits to, so an unstable serializer would make receipts unverifiable.
Prove a and b sequentially equivalent, or find a counterexample.
refute(clauses) -> {"unsat", "drat", "model"}; defaults to the bundled
solver. Pass equiv_receipt.solver.refute() for a real one.
Two arguments are attempted, in this order, and the receipt records which one carried the result:
- Register correspondence — the stronger invariant "corresponding latches hold equal values". Three obligations: it holds at reset, it is preserved by one step, and it implies equal outputs. When it goes through, one step of induction settles every time, which is why it is tried first.
- k-induction on outputs — the general argument, for designs whose state encodings do not correspond.
Base cases from the reset state are always run first, because a failure there is a real counterexample rather than an artefact of an unreachable assumed state.
Re-derive a sequential receipt from scratch. Never reads the verdict.
Four things are checked, and the third is the one a combinational receipt cannot do: the CNF of every obligation is re-encoded from the committed design and compared, so the formula is proven to be the circuits.