Reversible modular doubling — the verified value primitive a ↦ 2a mod N (ECDLP Phase 2, Stage S6.3d-1) #
Category: 1-Mathlib (CSD-free; staged as a Mathlib-upstream candidate).
This module verifies the modular-doubling VALUE primitive a ↦ 2a mod N over bit registers, the
acc ← 2·acc mod N step of the interleaved MSB-first modular multiply. It is realised as a copy
followed by the already-verified S6.3b modular adder:
modDouble L = copyReg L ++ modAdd L.addLayout
- Copy step
copyReg L(the only genuinely new gadget): withBholdingaand the operand registerAopinitially0on[0, n),nCNOTsCX (B i) (Aop i)copyBintoAop. EachAop i ← Aop i ⊕ B i = 0 ⊕ B i = B i;Bis the control, so it is preserved. After the copy:Aop = a,B = a. - Add step
modAdd L.addLayout(S6.3b,modAdd_correct): withAop = a,B = a(both< N), presetsA1 = 2ⁿ − N,A2 = N, carries / ancilla clean,2N ≤ 2ⁿ, leavesB ← (a + a) mod N, anda + a = 2 * agives2a mod N.
The copy step touches only the wires {B i, Aop i}. So it WRITES Aop (from 0 to a, exactly the
operand the add step wants), PRESERVES B (control), and leaves the add step's presets / carries
(A1, A2, Cadd, C1, C2, anc) untouched — re-establishing modAdd_correct's hypotheses with
a := a, b := a.
Carve line (what this is, and is NOT) #
This is 2a mod N realised as copy ++ modAdd, reusing wholesale the verified S6.3b modAdd.
The only new verified content is the copyReg CNOT-copy gadget (copyReg_correct). This is ℕ /
mod N bit arithmetic; no group-law / field semantics are in play.
Named residue (same fresh-ancilla model as S6.3b; do NOT read as a clean in-place primitive):
- Operand copy left dirty. After
modDouble, the operand registerAopis left holdinga(not restored to0), and — inherited frommodAdd— the carry chainsCadd,C1,C2and the comparison flag are dirty. Correctness holds because the layout supplies fresh wires per use. In- place reuse across many doublings needs the carry-clean / ancilla-restoring adders the corpus does NOT yet provide (Cuccaro-style inline uncompute, or the self-cleaning high-bit modular adder). - This is the doubling PRIMITIVE, not the multiply. The full interleaved MSB-first modular
MULTIPLY over
𝔽_p(the Horner loop ofmodDouble+cModAddwith the runningacc < Ninvariant) is the subsequent tranche (S6.3d-2). This module is theacc ← 2·acc mod Natom that loop iterates.
Honest cost #
modDouble_toffoli derives 12n Toffolis: copy step 0 (n CNOTs, no Toffoli) + add step 12n
(modularAdd_toffoli), composed through cost_comp_toffoli_count. The copy contributes n CNOTs
(modDouble_cnot_copy), which do not add Toffolis.
A modular-doubling layout on Fin m for n-bit registers. It is exactly a ModAddLayout: the
copy step reuses the same two registers the add step uses — B (holds a, the doubled accumulator)
and Aop (the operand, initially 0, copied to a). No new wire families are needed. All
disjointness / bounded-injectivity facts come for free from the bundled ModAddLayout.
- addLayout : ModAddLayout m n
The bundled S6.3b modular-addition layout (supplies every register + geometry fact).
Instances For
The copy gadget #
The copy circuit: n CNOTs CX (B i) (Aop i), copying register B into register Aop.
Equations
- Reversible.copyReg L = List.map (fun (i : ℕ) => Reversible.Gate.CX (L.B i) (L.Aop i)) (List.range n)
Instances For
Copy-step frame: a wire disjoint from {B i, Aop i} survives #
copyReg L touches only the wires {B k, Aop k} (the k-th gate is CX (B k) (Aop k), whose
gateWires is {B k, Aop k}). Any wire distinct from all of those passes through unchanged.
Copy-step value correctness #
The CNOTs have pairwise-disjoint targets (Aop injective on [0, n)) and never target a control
(hAopB), so each Aop i is touched by exactly its own CNOT, and B is never a target. We prove
the prefix invariant by induction: after the first k copies, Aop i = B i for i < k, Aop i
unchanged for k ≤ i, and B everywhere preserved.
The circuit of the first k copies.
Equations
- Reversible.copyPrefix L k = List.map (fun (i : ℕ) => Reversible.Gate.CX (L.B i) (L.Aop i)) (List.range k)
Instances For
The copy invariant. Assuming Aop is initially 0 on [0, n), after the first k copies:
Aop i = B i for i < k, Aop i is unchanged for k ≤ i < n, and B j is preserved for all j.
The B preservation holds because B is never a CNOT target (hAopB); the Aop clauses hold
because the targets are pairwise disjoint (Aop injective).
Copy correctness — operand. With Aop initially 0, after the copy the operand register
reads the value B held: regValRange Aop (denote (copyReg L) s) n = regValRange B s n.
Copy correctness — accumulator preserved. The copy leaves register B unchanged
(it is the control, never a CNOT target). Note B is NOT disjoint from the gate wires (each B k
is a control wire), so this is read off the invariant's B-preservation clause, not the generic
frame lemma.
Copy-step frame for the add-step presets / carries #
The add step (modAdd L.addLayout) reads A1, A2, Cadd, C1, C2, anc. Each is disjoint from both
B and Aop (the only wires copyReg touches), so all survive the copy and modAdd's preset /
clean-carry hypotheses re-establish. The hAop* / hB* fields of ModAddLayout supply the
disjointness; the helper below packages the per-wire frame for each.
Value correctness of the modular doubler #
The verified modular-doubling value primitive. For a ModDoubleLayout with operand register
Aop initially 0 on [0, n), accumulator B holding a < N, with 2N ≤ 2ⁿ, presets
A1 = 2ⁿ − N, A2 = N, and all carries (Cadd, C1, C2) and ancilla anc clean: modDouble L
leaves register B holding (2 * a) mod N.
Proof. The copy step (copyReg_correct_operand / copyReg_correct_B) writes a to Aop and
preserves B = a, and (copyReg_preserves_*) leaves the add step's presets A1 = 2ⁿ − N,
A2 = N and clean carries Cadd = C1 = C2 = false, anc = false. So modAdd_correct applies with
a := a, b := a, giving B ← (a + a) mod N; rewriting a + a = 2 * a finishes.
The modular-doubling output is a genuine residue in [0, N). Corollary of modDouble_correct
and Nat.mod_lt.
Derived cost #
Copy-step CNOT count: n CNOTs (and zero Toffoli). The copy is n CX gates.
Copy-step Toffoli count: zero — the copy is n CNOTs, no Toffoli.
Derived Toffoli cost of the modular doubler: 12n Toffolis, from the exhibited gate list.
Copy step 0 (copyReg_toffoli, the n CNOTs carry no Toffoli) + add step 12n
(modularAdd_toffoli), composed through cost_comp_toffoli_count.
Copy contributes n CNOTs to the doubler (reported separately; they do not add Toffolis).
Non-vacuity witness #
A concrete 3-bit modular-doubling layout on Fin 25, reusing the S6.3b modAddLayout2
(operand Aop → {0,1,2}, accumulator B → {3,4,5}, add-carry Cadd → {6..9}, reduce presets /
carries A1,C1,A2,C2,anc → {10..24}). n = 3 is needed (not n = 2): the modular adder requires
2N ≤ 2ⁿ, so for N = 3 that forces 2ⁿ ≥ 6, i.e. n ≥ 3.
The concrete runs double modulo N = 3: a = 2 ↦ (2 * 2) mod 3 = 1 and a = 1 ↦ (2 * 1) mod 3 = 2,
with the operand Aop initialised 0 (so the copy seeds it from B = a).
A concrete 3-bit modular-doubling layout on Fin 25, bundling modAddLayout2.
Equations
- Reversible.modDoubleLayout2 = { addLayout := Reversible.modAddLayout2 }
Instances For
Concrete input state for n = 3, N = 3: accumulator B = a (wires 3,4,5), operand Aop = 0
(wires 0,1,2), A1 = 5 = 2³ − 3 (wires 10,12), A2 = 3 (wires 17,18), all carries / ancilla
false. Parameterised by the data bits of a (on the B wires 3,4,5).
Equations
- One or more equations did not get rendered due to their size.
Instances For
The hypotheses of modDouble_correct hold at modDoubleState2 (operand 0, carries / ancilla
clear, A1 = 5, A2 = 3), for any data bits.
Fast #eval cross-check (Eval harness) #
runArr / regValRangeArr (Eval.lean) compute the same denote / regValRange value the
theorems are stated about (regValRangeArr_eq), but on a strict Array Bool — instant, no lazy
Function.update blowup. The #evals below print 2a mod N off register B for the two witnesses;
the decide examples confirm the printed numbers (kernel-reduced through the bridge).