The carry-clean (Θ(n)-qubit) MODULAR MULTIPLY X·Y mod N (ECDLP Phase 2, Stage 2b) #
Category: 1-Mathlib (CSD-free; staged as a Mathlib-upstream candidate).
Stage 2 (CuccaroModAdd.lean) delivered the carry-clean modular ADDER cuccaroModAdd
(Acc ← (a + b) mod N, every scratch wire restored). This module folds it into the carry-clean
modular MULTIPLY X · Y mod N with ONE reused scratch bank (Θ(n) qubits), versus the dirty
ModularMulLoop.mulLoop's Θ(n²) fresh-ancilla model.
Acc = 0; for each bit i of X MSB-first: Acc ← (2·Acc) mod N; Acc ← (Acc + X_i·Y) mod N.
The two clean sub-gadgets (each reuses the SAME scratch bank, re-cleaned per step) #
cuccaroModDouble— cleanAcc ← (2·Acc) mod N. Realised as an in-place shift + conditional subtract (Beauregard), NOT copy-add-uncopy. The copy-add-uncopy route is not clean: the copy register holdsa, which cannot be uncomputed onceAccbecomes2a mod N(no register then holdsa, anda = halve(2a mod N)needs a halver). The shiftrotChaindoubles by an information-preserving wire rotation (new[i] = old[i-1],new[0] = old[n] = 0), so there is no scratch to clean from the doubling itself; themod Nreduce reuses the Stage-2 pieces (cuccaroSub,maskCopy,cuccaroAdd). The comparison flag is uncomputed by parity: for oddN(the ECDLP prime case),[2a < N] = ¬(2a mod N) mod 2, so aCX (Acc 0) flag ; Xclears it. Load-bearing hypothesis:Nodd.cuccaroCModAdd— clean bit-gatedAcc ← (Acc + X_i·Y) mod N. The masked-operand trick:Mask2 ^= X_i·Y(maskCopyCtrl,nCCXs), runcuccaroModAddwith addendMask2, then uncomputeMask2 ^= X_i·Y. BecauseX_iandYare preserved by the modular add, the mask uncomputes cleanly. This avoids a controlled adder.
What is proved (sorry-free, foundational-triple-only) #
cuccaroModDouble_correct/_clean/_preserves_Nreg/_toffoli(=6n + 4).cuccaroCModAdd_correct/_clean/_preserves_operand/_toffoli(=14n + 10).cuccaroModMul_correct— generaln:Acc ← (X · Yval) mod N.cuccaroModMul_clean— the shared scratch bank restored (so the Θ(n) reuse is real).cuccaroModMul_preserves_XY—X,Yintact.cuccaroModMul_toffoli—(20n + 14)·n = 20n² + 14n(the headline; per-step20n + 14).
Honest cost / scope #
Per-multiply Toffoli 20n² + 14n (NOT the ~2n² of a non-modular multiply: the mod N reduce per
step is irreducible in this measurement-free CCX-only DSL). The prize is Θ(n) reusable qubits
(ONE shared scratch bank mod) vs the dirty mulLoop's Θ(n²) fresh ancilla; the per-multiply
Toffoli is also ~1.5× better than the dirty 30n². This is the verified modular field-multiply
atom; the elliptic-curve point op is a later stage. Load-bearing hypothesis: N odd (the parity
flag-uncompute in the doubler), which holds for the ECDLP prime field.
The in-place left-shift (doubling) rotation rotChain #
rotChain f k is the top-down adjacent-swap chain swap(f k, f(k-1)) :: ... :: swap(f 1, f 0). It
realises the cyclic up-rotation of the block [0, k]: new[i] = old[i-1] for 1 ≤ i ≤ k,
new[0] = old[k]. With old[k] = false this is multiplication by 2 of the low k bits.
Top-down adjacent-swap chain on register f (length k).
Equations
- Reversible.rotChain f 0 = []
- Reversible.rotChain f k.succ = Reversible.Gate.swap (f (k + 1)) (f k) :: Reversible.rotChain f k
Instances For
The rotation doubles. With f injective on [0, k] and s (f k) = false:
regValRange f (denote (rotChain f k) s) (k+1) = 2 * regValRange f s k.
Cost of the rotation: zero Toffoli (it is k swaps, each 3 CNOTs).
The clean modular doubler cuccaroModDouble #
Reuses the Stage-2 layout CuccaroModLayout (register B is unused by the doubler). The reduce
pieces (cuccaroSub L.layN, maskCopy L, cuccaroAdd L.layM) are exactly those of cuccaroModAdd.
The flag uncompute is CX (Acc 0) flag ; X flag (parity, odd N).
The full doubler spec (value + every scratch wire clean + Nreg preserved).
Correctness. cuccaroModDouble leaves Acc = (2·a) mod N.
The doubler is carry-clean. Flag, carry-out Acc[n], every Mask, and Z restored.
Nreg is preserved (low value and top padding wire).
Derived Toffoli cost: 6n + 4. Rotation 0 (swaps, no Toffoli), subtract 2(n+1),
mask n, add 2(n+1), mask n, single gates 0: 2(n+1) + n + 2(n+1) + n = 6n + 4.
Two cuccaroModAdd frame lemmas (external wires + per-wire B) #
cuccaroModAdd exports only regValRange-level operand preservation; the bit-gated adder needs
(a) preservation of wires entirely outside the modular adder (for the read-only Y / ctrl),
and (b) per-wire restoration of the addend register B (for the masked-operand uncompute).
Both compose the per-stage frame lemmas; the ancilla Z stays clean throughout (the per-stage
_preserves_Z lemmas are unconditional).
A wire disjoint from every modular-adder family (Acc, B, Nreg, Mask, flag, Z) survives
cuccaroModAdd.
Per-wire restoration of the addend B. Each B k (k < n + 1) is returned to its input
value (the carries threaded through it during the three cuccaroAdd L.layB / two cuccaroSub L.layB
passes are restored). Needs only s Z = false.
The control-masked copy maskCopyCtrl #
Mask2 ^= ctrl · Y via n Toffolis CCX ctrl (Y i) (B i) (the addend register B plays the role
of Mask2). Mirrors maskCopy with (flag, Nreg, Mask) ↦ (ctrl, Y, B).
First k masked-copy gates CCX ctrl (Y i) (B i).
Equations
- Reversible.maskCopyCtrlPrefix L Y ctrl k = List.map (fun (i : ℕ) => Reversible.Gate.CCX ctrl (Y i) (L.B i)) (List.range k)
Instances For
The full control-masked copy: Mask2 = B ^= ctrl · Y on all n low wires. Self-inverse.
Equations
- Reversible.maskCopyCtrl L Y ctrl = Reversible.maskCopyCtrlPrefix L Y ctrl n
Instances For
The control-masked-copy invariant. After k gates: B j (j < k) holds
B j ^^ (ctrl ∧ Y j); B j (k ≤ j ≤ n) is untouched; every non-B wire is preserved.
maskCopyCtrl top wire B n untouched.
maskCopyCtrl value: with B initially 0, Mask2 = B ends holding if ctrl then y else 0
where y = regValRange Y s n.
The clean bit-gated modular adder cuccaroCModAdd #
Bundled layout for the clean conditional modular add: the Stage-2 modular-adder layout mod
(its addend register mod.B is the masked operand Mask2), plus the read-only multiplicand Y
and the gating bit ctrl, disjoint from everything mod touches.
- mod : CuccaroModLayout m n
The carry-clean modular-adder layout (
mod.B= the masked operandMask2). The read-only multiplicand register (
nbits).- ctrl : Fin m
The gating bit.
Instances For
The full bit-gated-adder spec (value + all scratch incl. Mask2 clean + Y / ctrl / Nreg
preserved).
Correctness. The bit-gated modular adder leaves Acc = (a + (if ctrl then y else 0)) mod N.
The bit-gated adder is carry-clean, including the masked operand Mask2 = mod.B.
Derived Toffoli cost: 14n + 10. Two control-masks (n CCX each) + the modular adder
(12n + 10): n + (12n + 10) + n = 14n + 10.
A wire disjoint from Acc, Nreg, Mask, flag, Z survives cuccaroModDouble (it touches no other
families; the addend register B is not used by the doubler).
A wire disjoint from Acc, B, Nreg, Mask, flag, Z survives cuccaroCModAdd (the read-only Y
and ctrl are never written).
The carry-clean modular multiply: the Horner fold with ONE reused scratch bank #
An n-bit carry-clean modular-multiply layout on Fin m: a single Stage-2 modular-adder layout
mod (its addend register mod.B is the shared masked-operand scratch Mask2), a multiplicand
register Y, and a multiplier register X. Both Y and X are disjoint from everything mod
touches; X is injective on [0, n). This is ONE shared scratch bank — mod (with its
Acc, B, Nreg, Mask, flag, Z) is reused across all n Horner steps (Θ(n) qubits, not Θ(n²)).
- mod : CuccaroModLayout m n
The shared carry-clean modular-adder layout (
mod.B= the reused masked-operand scratch). The multiplicand register
Y(read-only,nbits).The multiplier register
X(read-only control bits; bitn-1-jgates stepj).
Instances For
The bit-gated-adder layout for step j (its gate is bit X (n-1-j), MSB-first).
Equations
- One or more equations did not get rendered due to their size.
Instances For
One Horner step on the shared bank: double the accumulator, then bit-gated-add X_{n-1-j}·Y.
Equations
Instances For
The general-n carry-clean modular multiply: fold the Horner step over the n multiplier bits
MSB-first, reusing the single scratch bank mod.
Equations
- Reversible.cuccaroModMul L = List.flatMap id (List.map (fun (j : ℕ) => Reversible.cuccaroModMulStep L j) (List.range n))
Instances For
The first k Horner steps (the induction handle).
Equations
- Reversible.cuccaroModMulUpto L k = List.flatMap id (List.map (fun (j : ℕ) => Reversible.cuccaroModMulStep L j) (List.range k))
Instances For
The multiply-loop invariant. After the first k Horner steps from a clean bank
(Acc = 0), the accumulator holds (hornerVal bits n k · y) mod N (bits i = [X i]), the whole
scratch bank is restored clean, and Nreg, Y, every X bit are intact. By induction on k.
The verified general-n carry-clean modular multiply. From a clean shared bank
(Acc = 0, all scratch false, Nreg = N, Y = Yval < N, 2N ≤ 2ⁿ, N odd), the loop leaves the
accumulator holding (X · Yval) mod N, with the multiplier X arbitrary.
The shared scratch bank is restored clean (so the Θ(n) reuse is real): the carry-out
Acc[n], flag, every Mask, every Mask2 = mod.B, and Z all return to false.
X and Y are intact (and Nreg = N): the multiplier / multiplicand survive the multiply.
Derived cost: (20n + 14)·n = 20n² + 14n Toffolis #
Derived Toffoli cost: (20n + 14)·n = 20n² + 14n. n Horner steps, each 20n + 14
(doubler 6n + 4 + bit-gated adder 14n + 10). The prize is the Θ(n) reusable qubit count
(one shared scratch bank), not the Toffoli constant (modular reduction per step is irreducible in
this measurement-free CCX-only DSL); still ~1.5× better than the dirty mulLoop's 30n².
Non-vacuity witness + #eval / runArr cross-check (n = 3, N = 3) #
A concrete CuccaroMulLayout 24 3: Acc → {0,1,2,3}, the shared masked-operand scratch
Mask2 = mod.B → {4,5,6,7}, Nreg → {8,9,10,11} (preset N = 3 on wires 8,9),
Mask → {12,13,14,15}, flag → 16, Z → 17, multiplicand Y → {18,19,20} (preset 2 on wire
19), multiplier X → {21,22,23}. n = 3 is forced by 2N ≤ 2ⁿ (N = 3 needs 2ⁿ ≥ 6). The
witnesses realise X · 2 mod 3: X = 3 ↦ 0, X = 2 ↦ 1, X = 1 ↦ 2, read off the strict Array
evaluator (runArr, via regValRangeArr_eq), with the shared scratch reading clean afterward.
A concrete n = 3 carry-clean modular-adder layout on Fin 24 (the shared bank).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Interface lemmas (CONVENTIONS §9.1, F1): the wire assignments of the concrete layout, one lemma per field.
cuccaroMulModLayout3's comparison-flag wire.
cuccaroMulModLayout3's borrow/carry ancilla wire.
A concrete n = 3 carry-clean modular-multiply layout on Fin 24.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The structural preconditions of cuccaroModMul_correct hold at cuccaroMulState3.