logicaffeine_compile/codegen_sva/pnr_certificate.rs
1//! Proof-carrying place-and-route.
2//!
3//! A dataflow compiler that only *reports* a mapping — the way a spatial-architecture toolchain validates a
4//! placement with performance counters — asks you to TRUST that the placed grid computes the intended
5//! datapath. We instead SHIP A CERTIFICATE: a machine-checkable refutation that the placed grid equals its
6//! datapath specification for **every** bounded input. The recipient (e.g. the silicon vendor) re-verifies it
7//! with [`PlacementCertificate::recheck`], trusting only the tiny PR/RUP checker in
8//! [`logicaffeine_proof::pr`] — never our prover, our placer, or our word. That is the difference between
9//! "we validated it" and "here is the proof; check it yourself."
10//!
11//! The obligation is the same `grid ≡ datapath` biconditional the SAT-certified placer already discharges;
12//! here we keep the *witness* (the propagation-redundancy + RUP steps) instead of discarding it, so the
13//! certificate travels with the emitted RTL.
14
15use logicaffeine_proof::cdcl::Lit;
16use logicaffeine_proof::cnf::Cnf;
17use logicaffeine_proof::pr::check_pr_refutation;
18use logicaffeine_proof::proof::{ProofStep, Witness};
19use logicaffeine_proof::sat::prove_unsat_certified_cnf;
20
21use super::bitblast::{
22 assert_not, blast_pred, pred_size, BlastRefusal, Pred, Word, BLAST_BUDGET_NODES,
23};
24use super::fabric_grid::{grid_output_to_bounded, Grid};
25use super::fabric_rtl::{fabric_to_bounded, FabricOp};
26use super::sva_to_verify::BoundedExpr;
27
28/// A machine-checkable certificate that a placed mapping `grid` computes its datapath spec for every bounded
29/// assignment. It carries the refuted CNF (`¬(grid ≡ datapath)`) and the propagation-redundancy + RUP
30/// refutation of it. It is self-contained: [`recheck`](Self::recheck) needs nothing but the certificate.
31#[derive(Clone, Debug)]
32pub struct PlacementCertificate {
33 num_vars: usize,
34 cnf: Vec<Vec<Lit>>,
35 steps: Vec<ProofStep>,
36}
37
38impl PlacementCertificate {
39 /// Re-verify the certificate INDEPENDENTLY. Trusts only the small PR/RUP checker — not the prover, placer,
40 /// or emitter that produced it. This is the audit a third party runs to confirm our place-and-route
41 /// without re-deriving anything: it replays the refutation against the original CNF and returns whether
42 /// every step is redundancy-sound and the empty clause is reached.
43 pub fn recheck(&self) -> bool {
44 check_pr_refutation(self.num_vars, &self.cnf, &self.steps)
45 }
46
47 /// The number of refutation steps carried — the size of the shipped proof.
48 pub fn len(&self) -> usize {
49 self.steps.len()
50 }
51
52 pub fn is_empty(&self) -> bool {
53 self.steps.is_empty()
54 }
55
56 /// The number of Boolean variables in the refuted obligation.
57 pub fn num_vars(&self) -> usize {
58 self.num_vars
59 }
60
61 /// A stable 64-bit digest of the whole certificate — the refuted CNF and every refutation step, witnesses
62 /// included (FNV-1a over the literal codes) — so a summary that travels without the object is bound to
63 /// it: a dropped step, a changed clause or a different obligation is a different digest.
64 pub fn digest(&self) -> u64 {
65 fn mix(h: &mut u64, x: u64) {
66 *h ^= x;
67 *h = h.wrapping_mul(0x0000_0100_0000_01b3);
68 }
69 fn code(l: Lit) -> u64 {
70 ((l.var() as u64) << 1) | u64::from(!l.is_positive())
71 }
72 fn clause(h: &mut u64, c: &[Lit]) {
73 mix(h, c.len() as u64);
74 for l in c {
75 mix(h, code(*l));
76 }
77 }
78 let mut h = 0xcbf2_9ce4_8422_2325u64;
79 mix(&mut h, self.num_vars as u64);
80 mix(&mut h, self.cnf.len() as u64);
81 for c in &self.cnf {
82 clause(&mut h, c);
83 }
84 mix(&mut h, self.steps.len() as u64);
85 for step in &self.steps {
86 match step {
87 ProofStep::Rup(c) => {
88 mix(&mut h, 1);
89 clause(&mut h, c);
90 }
91 ProofStep::Pr { clause: c, witness } => {
92 mix(&mut h, 2);
93 clause(&mut h, c);
94 match witness {
95 Witness::Assignment(ls) => {
96 mix(&mut h, 3);
97 clause(&mut h, ls);
98 }
99 Witness::Substitution(perm) => {
100 mix(&mut h, 4);
101 mix(&mut h, perm.num_vars() as u64);
102 for v in 0..perm.num_vars() {
103 mix(&mut h, code(perm.apply(Lit::pos(v as u32))));
104 }
105 }
106 }
107 }
108 ProofStep::Delete(c) => {
109 mix(&mut h, 5);
110 clause(&mut h, c);
111 }
112 }
113 }
114 h
115 }
116
117 /// A tampered copy: the final refutation step is dropped, so the empty clause is no longer derived. Used to
118 /// prove the checker is load-bearing — [`recheck`](Self::recheck) on the result must return `false`.
119 #[must_use]
120 pub fn with_last_step_removed(&self) -> Self {
121 let mut steps = self.steps.clone();
122 steps.pop();
123 Self {
124 num_vars: self.num_vars,
125 cnf: self.cnf.clone(),
126 steps,
127 }
128 }
129}
130
131/// Emit a proof-carrying certificate that the placed `grid` equals the `datapath` spec for every bounded
132/// input, by refuting `¬(grid ≡ datapath)` and KEEPING the checkable refutation. Fail-closed: returns `None`
133/// when the equivalence does not actually hold (a wrong mapping earns no certificate) or the obligation
134/// leaves the propositional fragment (bit-blast the datapath first). A returned certificate is guaranteed
135/// [`recheck`](PlacementCertificate::recheck)-valid by construction — the underlying prover only hands back a
136/// refutation it has already PR/RUP-verified.
137pub fn certify_mapping(grid: BoundedExpr, datapath: BoundedExpr) -> Option<PlacementCertificate> {
138 certify_mapping_bounded(grid, datapath).ok()
139}
140
141/// Why a mapping earned no certificate. The three answers are genuinely different — "we cannot
142/// express this obligation", "we will not pay to materialise it", and "it is not true" — and a
143/// caller that prints `Uncertified { reason }` must not collapse them into one sentence.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum CertifyRefusal {
146 /// The obligation never reached a formula: no propositional form, or over the bit-blast budget.
147 Blast(BlastRefusal),
148 /// The formula was built but could not be refuted — the mapping is NOT equivalent to the spec,
149 /// or the prover declined it. Fail-closed: a wrong mapping earns no certificate.
150 NotRefuted,
151}
152
153impl CertifyRefusal {
154 /// The prose a certificate carries when it declines. Names the cause — and, for a budget
155 /// refusal, the budget itself, so the reader can tell a limit from a failure.
156 pub fn reason(&self) -> String {
157 match self {
158 CertifyRefusal::Blast(b) => b.reason(),
159 CertifyRefusal::NotRefuted => {
160 "the placement obligation could not be refuted".to_string()
161 }
162 }
163 }
164}
165
166/// [`certify_mapping`] with the refusal NAMED. The obligation is PRICED in the shared gate DAG
167/// before any `ProofExpr` is materialised, so an encoding too big to hold fails as a typed refusal
168/// in milliseconds instead of walking the allocator off a cliff.
169pub fn certify_mapping_bounded(
170 grid: BoundedExpr,
171 datapath: BoundedExpr,
172) -> Result<PlacementCertificate, CertifyRefusal> {
173 certify_mapping_within(grid, datapath, BLAST_BUDGET_NODES)
174}
175
176/// [`certify_mapping_bounded`] against an EXPLICIT bit-blast ceiling — the seam a lock shrinks to
177/// prove the budget is live code and not a constant nobody reads.
178pub fn certify_mapping_within(
179 grid: BoundedExpr,
180 datapath: BoundedExpr,
181 budget: u128,
182) -> Result<PlacementCertificate, CertifyRefusal> {
183 let eq = BoundedExpr::Eq(Box::new(grid), Box::new(datapath));
184 let p = blast_pred(&eq).ok_or(CertifyRefusal::Blast(BlastRefusal::NotBounded))?;
185 let size = pred_size(&p);
186 if size.tree_nodes > budget {
187 return Err(CertifyRefusal::Blast(BlastRefusal::Budget { size, budget }));
188 }
189 refute_miter(&p)
190}
191
192/// How large a SHARED GATE DAG the certificate path will pay to refute.
193///
194/// This is the ceiling that stays live once the obligation is built as a circuit rather than as a
195/// tree. [`BLAST_BUDGET_NODES`] prices the `ProofExpr` TREE, which is the right question only for
196/// a caller that materialises one; the gate path never does, so what it must bound is the number
197/// of gates it allocates and Tseitin-encodes — both linear in the DAG. The RDU demo corpus's
198/// widest body (`fib.lg`'s six-block fast-doubling chain, 75 cells of chained 8-bit multiplies)
199/// prices in the tens of thousands of gates once the grid and the spec are built as DAGs, so two
200/// million is orders of magnitude of headroom over anything the product lowers today, and a body
201/// past it is refused BY NAME in the time it takes to build the circuit — never by the allocator.
202/// `certifier_strength_lock` re-measures both sides of that claim.
203pub const GATE_BUDGET_NODES: u64 = 2_000_000;
204
205/// Certify a placement whose grid and datapath are ALREADY blasted into the shared gate DAG — the
206/// path that never materialises a `ProofExpr` tree, and therefore the only one that can certify a
207/// reconvergent datapath (a multiply chain's tree is exponentially larger than its circuit).
208/// Fail-closed exactly like [`certify_mapping`]: a grid that is not equivalent to its spec earns
209/// no certificate.
210pub fn certify_gates(grid: &Word, datapath: &Word) -> Option<PlacementCertificate> {
211 certify_gates_bounded(grid, datapath).ok()
212}
213
214/// [`certify_gates`] with the refusal NAMED, against the production [`GATE_BUDGET_NODES`].
215pub fn certify_gates_bounded(
216 grid: &Word,
217 datapath: &Word,
218) -> Result<PlacementCertificate, CertifyRefusal> {
219 certify_gates_within(grid, datapath, GATE_BUDGET_NODES)
220}
221
222/// [`certify_gates_bounded`] against an EXPLICIT gate ceiling — the seam a lock shrinks to prove
223/// the budget is live code and not a constant nobody reads.
224pub fn certify_gates_within(
225 grid: &Word,
226 datapath: &Word,
227 budget: u64,
228) -> Result<PlacementCertificate, CertifyRefusal> {
229 let p = super::bitblast::words_equal(grid, datapath)
230 .ok_or(CertifyRefusal::Blast(BlastRefusal::NotBounded))?;
231 let size = pred_size(&p);
232 if size.dag_nodes > budget {
233 return Err(CertifyRefusal::Blast(BlastRefusal::GateBudget {
234 dag_nodes: size.dag_nodes,
235 budget,
236 }));
237 }
238 refute_miter(&p)
239}
240
241/// Refute `¬(grid ≡ datapath)` and KEEP the checkable refutation.
242///
243/// The CNF is clausified STRAIGHT FROM THE GATE DAG — the same clauses, in the same order, over
244/// the same variables that `Cnf::assert` produces from the materialised tree, and it is built
245/// ONCE and handed to the prover, rather than built here and built again inside it.
246fn refute_miter(p: &Pred) -> Result<PlacementCertificate, CertifyRefusal> {
247 let mut cnf = Cnf::new();
248 assert_not(p, &mut cnf);
249 let steps = prove_unsat_certified_cnf(cnf.num_vars(), cnf.clauses())
250 .ok_or(CertifyRefusal::NotRefuted)?;
251 Ok(PlacementCertificate {
252 num_vars: cnf.num_vars(),
253 cnf: cnf.clauses().to_vec(),
254 steps,
255 })
256}
257
258/// **Import-and-certify** — the SDK-facing seam. Certify a placement PRODUCED BY AN EXTERNAL TOOL (e.g. the
259/// SambaFlow place-and-route) against the English `source` datapath: lower the external `placement` grid and
260/// the `source` spec to the same bounded fragment, then prove they agree for every input and keep the
261/// re-checkable witness. Returns `None` (fail-closed) if the placement is NOT equivalent to the source, or the
262/// obligation escapes the propositional fragment. We need nothing from the vendor but the placed grid — no
263/// config format, no trust in their placer: the certificate stands on its own via
264/// [`PlacementCertificate::recheck`]. This is how a vendor mapping earns a proof it computes the intended
265/// program.
266pub fn certify_external_placement(
267 placement: &Grid,
268 source: &[FabricOp],
269 result: usize,
270 width: u32,
271) -> Option<PlacementCertificate> {
272 let placed = grid_output_to_bounded(placement, 0, width, &|k| format!("in{k}"));
273 let spec = fabric_to_bounded(source, result, width);
274 certify_mapping(placed, spec)
275}