Reversible interleaved modular multiply โ the verified Horner LOOP BODY (ECDLP Phase 2, Stage S6.3d-2a) #
Category: 1-Mathlib (CSD-free; staged as a Mathlib-upstream candidate).
This module verifies the loop body of the interleaved MSB-first modular multiply over ๐ฝ_p,
acc โ (2ยทacc + [X_i]ยทY) mod N, by chaining the two already-verified blocks one tranche below:
hornerStep L = modDouble L.dbl ++ cModAdd L.add
- modDouble
modDouble L.dbl(S6.3d-1,modDouble_correct/modDouble_in_range): with the accumulatorBholdingc < N, its scratch operanddbl.Aopinitialised0, presetsA1 = 2โฟ โ N,A2 = N, clean carries / ancilla, and2N โค 2โฟ, it leavesB โ (2c) mod N, with(2c) mod N < N. It touches only its own wires{B, dbl.Aop, dbl.Cadd, dbl.A1, dbl.C1, dbl.A2, dbl.C2, dbl.anc}, so the multiplicandY = add.Aop, the control bitX_i = add.ctrl, and the controlled-add step's fresh carries / presets / ancilla all survive it (the frame lemmasmodDouble_preserves_externaland corollaries below). - cModAdd
cModAdd L.add(S6.3c,cModAdd_correct): nowB = d := (2c) mod N < N, operandAop = Y(Yval < N), controlctrl = X_i. It leavesB โ if X_i then (Yval + d) mod N else d.
Composition (the loop-body identity, absorbing the inner mod N via Nat.add_mod / Nat.mod_add_mod):
X_iset:(Yval + (2c mod N)) mod N = (Yval + 2c) mod N = (2c + Yval) mod N.X_iclear:(2c) mod N = (2c + 0) mod N.
So hornerStep_correct gives B โ (2*c + (if X_i then Yval else 0)) mod N.
Carve line (what this is, and is NOT) #
This is the verified LOOP BODY of the interleaved MSB-first modular multiply,
acc โ (2ยทacc + [X_i]ยทY) mod N, composing the verified modDouble (S6.3d-1) and cModAdd (S6.3c).
The 2-step composition is delivered two ways: (i) the proven mulStep2_correct, which chains
hornerStep_correct twice over two banks sharing B and Y with fresh per-bank wires, concluding
acc = (X ยท Yval) mod N for the 2-bit multiplier X = 2ยทXโ + Xโ; and (ii) a concrete Fin 92
instance (mulCircuit2 / mulState2) whose three #eval / decide witnesses realise
X = 3 โฆ 0, X = 2 โฆ 1, X = 1 โฆ 2 at Y = 2, N = 3 (the verified n = 2 modular multiply).
This is NOT the general-n multiply. The general-n Horner loop โ the induction folding
hornerStep over all n bits of X with the invariant acc = (X โซ i)ยทY mod N โ is the subsequent
tranche S6.3d-2b, NOT built here โ it lives in ModularMulLoop.lean (mulLoop_correct).
The fresh-ancilla / dirty-carry model (inherited from S6.3d-1 and S6.3c):
- Fresh per-iteration wires (O(nยฒ) qubits). Each Horner step is supplied its OWN doubling
scratch / carries / ancilla and its OWN controlled-add carries / ancilla, disjoint from the
previous step's. The 2-step demo exhibits exactly this: two banks, fresh wires each. Across
nsteps this isฮ(nยฒ)ancilla. In-place reuse (ฮ(n)qubits) needs a carry-clean / ancilla-restoring adder โ supplied byCuccaroModAdd.lean(cuccaroModAdd_clean) and folded into theฮ(n)-qubit multiply inCuccaroModMul.lean; NOT built here. - Only the multiplicand
Ymust persist across steps. The loop reads eachX_ionce (the bit wire of stepiis dead after stepi), butY = add.Aopmust survive every step; this is the load-bearinghornerStep_preserves_Y.
Honest cost #
hornerStep_toffoli derives 30n Toffolis from the exhibited gate list: modDouble 12n
(modDouble_toffoli) + cModAdd 18n (cModularAdd_toffoli), composed through
cost_comp_toffoli_count.
Generic frame: a wire external to modReduce / modAdd / modDouble survives #
modDouble L = copyReg L ++ modAdd L.addLayout and modAdd L = rippleCirc L.addStep ++ modReduce L.reduceStep. Each block is a frame lemma over its wire families; the three lemmas below compose them
so that a wire disjoint from every modDouble-touched family passes through unchanged. These are the
only genuinely new structural lemmas of this tranche.
modReduce frame. A wire w disjoint from every reduce-step family
(B, A1, C1, A2, C2, anc) is left unchanged by modReduce L. Mirrors
modReduce_reduceStep_preserves_Aop but abstracted to an arbitrary external wire and a bare
ModReduceLayout.
modAdd frame. A wire w disjoint from every modAdd-touched family
(Aop, B, Cadd, A1, C1, A2, C2, anc) is left unchanged by modAdd L. Composes
rippleCirc_addStep_preserves (add step) with modReduce_preserves_external (reduce step).
modDouble frame. A wire w disjoint from every modDouble-touched family
(B, the scratch operand dbl.Aop, and the bundled add layout's Cadd, A1, C1, A2, C2, anc) is
left unchanged by modDouble L. Composes copyReg_preserves (copy step) with
modAdd_preserves_external (the bundled modAdd).
cModAdd frame. A wire w disjoint from every cModAdd-touched family
(Aop, B, Ccadd, ctrl, ancC of the controlled add, and A1, C1, A2, C2, anc of the reduce) is left
unchanged by cModAdd L. Composes cRippleCirc_preserves_external (controlled add step) with
modReduce_preserves_external (reduce step). Register hyps are bounded (< n / < n + 1), as in
cRippleCirc_preserves_external.
The Horner-step layout #
A HornerStepLayout bundles the S6.3d-1 doubling sub-layout dbl and the S6.3c controlled-add
sub-layout add, sharing the accumulator (dbl.B = add.B, the running acc), with the doubling's
wires (scratch operand + carries + ancilla) and the controlled-add's wires (carries + ancilla)
fresh and disjoint from each other, from the multiplicand Y = add.Aop, and from the control bit
X_i = add.ctrl. The cross-disjointness fields are exactly what modDouble_preserves_external needs
to re-establish every cModAdd_correct hypothesis through the doubling.
A Horner-step layout for n-bit registers on Fin m. The two sub-layouts share the accumulator
B; everything else of the doubling block is disjoint from everything the controlled-add block reads
or carries.
- dbl : ModDoubleLayout m n
The S6.3d-1 doubling sub-layout (its
Bis the shared accumulator). - add : CModAddLayout m n
Instances For
The control wire X_i for this Horner step (the controlled-add's ctrl).
Instances For
The Horner-step circuit. Double the accumulator (modDouble), then conditionally add the
multiplicand Y controlled on the bit X_i (cModAdd).
Equations
Instances For
modDouble preserves the controlled-add block's external wires #
Each corollary specialises modDouble_preserves_external to one controlled-add family, feeding the
eight cross-disjointness fields for that family. These re-establish cModAdd_correct's hypotheses at
the post-doubling state.
hornerStep frame. A wire w disjoint from every wire of BOTH blocks survives the whole
Horner step (modDouble L.dbl ++ cModAdd L.add). Used by the 2-step demo to transport bank 2's clean
presets / scratch / carries through bank 1. The modDouble families are
{B, dbl.Aop, dbl.Cadd, dbl.A1, dbl.C1, dbl.A2, dbl.C2, dbl.anc}; the cModAdd families are
{add.Aop, add.B, add.Ccadd, add.ctrl, add.ancC, add.A1, add.C1, add.A2, add.C2, add.anc}.
Value correctness of the Horner step #
The verified Horner loop body acc โ (2ยทacc + [X_i]ยทY) mod N. For a HornerStepLayout with
the accumulator B holding c < N, the multiplicand register Y = add.Aop holding Yval < N,
2N โค 2โฟ, both sub-layouts' presets (A1 = 2โฟ โ N, A2 = N), all carries / ancillas clean, and
the scratch operands of the doubling zeroed, hornerStep L leaves register B holding
(2ยทc + (if X_i then Yval else 0)) mod N.
Proof. The doubling step (modDouble_correct / modDouble_in_range) writes (2c) mod N < N to B,
preserves Y, X_i, and the controlled-add block's presets / clean carries / ancillas
(modDouble_pres_add*). The controlled add (cModAdd_correct) then writes
if X_i then (Yval + (2c mod N)) mod N else (2c mod N). In the set branch
Nat.add_mod_mod absorbs the inner reduction: (Yval + (2c mod N)) mod N = (Yval + 2c) mod N = (2c + Yval) mod N; in the clear branch (2c) mod N = (2c + 0) mod N.
The Horner-step output is a genuine residue in [0, N). Corollary of hornerStep_correct
and Nat.mod_lt.
The multiplicand Y is preserved. hornerStep L leaves Y = add.Aop at its initial value:
the doubling block is disjoint from Y (modDouble_pres_addAop), and the controlled add reads Y
read-only (cModAdd_preserves_operand). This is the load-bearing persistence: the loop reuses the
SAME Y across all bit positions.
Derived cost #
Derived Toffoli cost of the Horner step: 30n Toffolis, from the exhibited gate list.
Doubling step 12n (modDouble_toffoli) + controlled add step 18n (cModularAdd_toffoli),
composed through cost_comp_toffoli_count.
2-step composition: the verified n = 2 modular multiply #
Two Horner-step banks L1 (high bit Xโ) then L2 (low bit Xโ) sharing the accumulator B and
the multiplicand Y, with fresh per-bank scratch / carries / ancilla. Starting from acc = 0:
- step 1 leaves
accโ = (XโยทYval) mod N(hornerStep_correct L1,c = 0); - step 2 leaves
accโ = (2ยทaccโ + XโยทYval) mod N = ((2ยทXโ + Xโ)ยทYval) mod N = (XยทYval) mod N(hornerStep_correct L2,c = accโ), whereX = 2ยทXโ + Xโis the 2-bit multiplier.
The load-bearing inter-bank obligation is that bank 1 leaves bank 2's clean presets / scratch /
carries / ancilla and the low control bit Xโ UNTOUCHED โ taken here as the hp_* hypotheses, each
dischargeable from inter-bank disjointness via hornerStep_preserves_external (the fresh-wire model;
see the mulCircuit2 #eval cross-check for a concrete instance that satisfies them). This exhibits
that the loop body composes; the general-n Horner induction is S6.3d-2b, NOT proved here.
Concrete #eval cross-check: the verified n = 2 modular multiply on Fin 92 #
Two Horner-step banks laid out on Fin 92, n = 3 register width (forced by 2N โค 2โฟ for N = 3),
sharing the accumulator B โ {0,1,2} and the multiplicand Y โ {3,4,5}, with a 2-bit multiplier
X on the dedicated control wires {6, 7} (wire 6 = bank-1 control Xโ, the HIGH bit; wire 7 =
bank-2 control Xโ, the LOW bit). Every other wire family is FRESH per bank (the ฮ(nยฒ)-ancilla
fresh-wire model): bank 1 on {8..49}, bank 2 on {50..91}.
The full circuit is modDouble dbl1 ++ cModAdd add1 ++ modDouble dbl2 ++ cModAdd add2 โ exactly
hornerStep (bank 1) followed by hornerStep (bank 2). Reading register B (low 3 bits) off the
strict Array Bool evaluator (runArr, via the proven bridge regValRangeArr_eq) gives the value
the chained hornerStep_correct / mulStep2_correct constrain, computed instantly. The three
witnesses below realise X ยท Yval mod N for Yval = 2, N = 3: X = 3 โฆ 0, X = 2 โฆ 1,
X = 1 โฆ 2.
Bank-1 doubling sub-layout (scratch {8,9,10}, carries/presets {11..29}).
Equations
- One or more equations did not get rendered due to their size.
Instances For
Bank-2 doubling sub-layout (scratch {50,51,52}, carries/presets {53..71}).
Equations
- One or more equations did not get rendered due to their size.
Instances For
The full 2-step modular-multiply circuit: bank-1 Horner step then bank-2 Horner step.
Equations
Instances For
Concrete input state on Fin 92: shared accumulator B = 0 (wires {0,1,2}), multiplicand
Y = (y0,y1,y2) (wires {3,4,5}), 2-bit multiplier X = high bit x1 (wire 6) + low bit x0
(wire 7); both banks' presets A1 = 5 = 2ยณ โ 3 and A2 = 3; all scratch / carries / ancillas
false. Parameterised by the data bits.
Equations
- One or more equations did not get rendered due to their size.