Reversible interleaved modular multiply β the general-n Horner LOOP (ECDLP Phase 2, Stage S6.3d-2b) #
Category: 1-Mathlib (CSD-free; staged as a Mathlib-upstream candidate).
This module delivers the verified general-n interleaved MSB-first modular multiply over π½_p,
folding the verified Horner loop body (hornerStep, S6.3d-2a) over all n bits of the multiplier X
to leave the accumulator holding X Β· Y mod N. It is the capstone of the modular-arithmetic chain:
the verified field-multiply atom β¦cβ§ = (Β· * Yval) mod N an exhibited EC point op would call.
mulLoop L = ((List.range n).map (fun j => hornerStep (L.bank j))).flatMap id
processing bits MSB-first: loop index j = 0, β¦, n-1 runs hornerStep on bank j, whose control
is bound to X (n-1-j) β the (n-1-j)-th bit of the multiplier. Each body is one verified Horner step
acc β (2Β·acc + [X_{n-1-j}]Β·Y) mod N (hornerStep_correct). The banks share the accumulator B and the
multiplicand Y; every other wire of each bank is FRESH and disjoint (the Ξ(nΒ²)-ancilla fresh-wire
model inherited from S6.3d-1 / S6.3c).
The invariant (induction over the processed-bank prefix) #
After processing banks [0, β¦, k-1] from acc = 0, the accumulator holds hornerVal k Β· Yval mod N,
where hornerVal k is the top-k-bits-of-X MSB-first reconstruction
(hornerVal 0 = 0, hornerVal (k+1) = 2Β·hornerVal k + bit (n-1-k)). The step k β k+1 is exactly the
per-step reasoning of mulStep2_correct: hornerStep_correct on bank k maps
(hornerVal k Β· Yval) mod N to ((2Β·hornerVal k + bit)Β·Yval) mod N = (hornerVal (k+1) Β· Yval) mod N;
hornerStep_preserves_Y keeps Y; hornerStep_preserves_external keeps the later banks' wires clean and
the not-yet-read X bits intact. The bridge hornerVal n = Xval (hornerVal_full) closes it.
Carve line (what this is, and is NOT) #
This is the VERIFIED modular field-multiply X Β· Y mod N β the β¦cβ§ = op payoff of the S6.3
Option-1 route β composing the verified Horner step (S6.3d-2a) over all n bits.
Named residue:
- Fresh per-iteration wires βΉ
Ξ(nΒ²)qubits +Ξ(nΒ²)Toffoli. Each of thenHorner steps is supplied its OWN doubling scratch / carries / ancilla and controlled-add carries / ancilla, disjoint from every other step's. This is the honest fresh-ancilla cost. In-place reuse (Ξ(n)qubits) needs the carry-clean / ancilla-restoring adder the corpus does NOT yet provide (Cuccaro-style inline carry-uncompute, or the self-cleaning high-bit modular adder). That carry-clean adder is the genuine orthogonal residue. - This is NOT the EC point operation. It is the verified modular MULTIPLY over registers
(
X Β· Y mod N),β/mod Nbit arithmetic β no field / group semantics. Assembling these field-multiplies into the full elliptic-curve point op (point add / double) is S6.3e+, NOT built here. And this is theΞ(nΒ²)-qubit version, not the optimised in-place one.
Honest cost #
mulLoop_toffoli derives 30 * nΒ² Toffolis: n Horner steps, each 30n (hornerStep_toffoli),
composed through cost_comp_toffoli_count over the fold (multiplier_toffoli).
The Horner reconstruction value (pure β) #
hornerVal bits n k is the top-k-bits MSB-first reconstruction of the n-bit number whose bits are
bits 0, β¦, bits (n-1) (each bits i β {0,1} for the intended use, but the lemmas hold for any bits).
The bridge hornerVal_full (hornerVal bits n n = β_{i<n} bits i Β· 2^i) lets the loop's per-step
recurrence land on the full multiplier value.
Top-k-bits MSB-first reconstruction: hornerVal bits n 0 = 0,
hornerVal bits n (k+1) = 2 Β· hornerVal bits n k + bits (n-1-k).
Equations
- Reversible.hornerVal bits n 0 = 0
- Reversible.hornerVal bits n k.succ = 2 * Reversible.hornerVal bits n k + bits (n - 1 - k)
Instances For
The Horner-reconstruction bridge (auxiliary, all prefixes). For k β€ n, the top-k-bits
reconstruction times 2^(n-k), plus the bottom n-k bits in place value, is the full value.
The Horner-reconstruction bridge. The full n-bit MSB-first reconstruction equals the place-value
sum β_{i<n} bits i Β· 2^i. Specialised from hornerVal_aux at k = n.
The n-bank multiply-loop layout #
A MulLoopLayout bundles n per-bit HornerStepLayout banks (bank j, S6.3d-2a), all sharing the
accumulator B ((bank j).B = B) and the multiplicand Y ((bank j).Y = Y), with bank j's control
bound to X (n-1-j) (MSB-first). The inter-bank geometry needed to fold the steps β bank j's circuit
must not touch any of bank k's clean / preset wires (k β k), nor the not-yet-read control bits β is
carried by the membership-based footprints Touches / Clean and the single disjointness field
hInter. This is bounded and inhabitable for every n (no unbounded β β Fin m injectivity
field): the witness assigns each bank a disjoint contiguous block of fresh wires and discharges hInter
/ hCtrl* by omega, and Touches/Clean are decidable finite disjunctions over the 18/14 families.
Touches L j w holds iff w is one of bank j's 18 touched wire families; Clean L k w iff w is one
of bank k's 14 clean/preset families (the ones whose initial value bank k's hornerStep_correct
reads). The field hInter says: for j β k, a clean wire of bank k is not touched by bank j β which
is exactly what hornerStep_preserves_external needs to transport bank k's preconditions through bank
j's step.
The 14 wire families bank k's hornerStep_correct reads as a clean / preset precondition: the
doubling scratch / carries / ancilla / presets and the controlled-add carries / ancilla / presets. (The
shared B, Y and the control bit are handled separately.)
Equations
- One or more equations did not get rendered due to their size.
Instances For
An n-bank modular-multiply loop layout on Fin m. The n banks share B and Y; bank j's
control is X (n-1-j); bank j does not touch bank k's clean wires for k β j (hInter), nor any
control bit X i other than its own (hCtrlTouch), and X/B/Y registers are injective on [0,n).
All hypotheses are bounded (< n), so the schema is inhabitable for every n.
- bank : β β HornerStepLayout m n
The
nper-bit Horner-step banks (bank jprocesses bitn-1-j). Bank
j's control bit isX (n-1-j).- hInter (j k : β) (w : Fin m) : j < n β k < n β j β k β Clean self.bank k w β Β¬Touches self.bank j w
Inter-bank disjointness: for
j β k, bankj's circuit does not touch bankk's clean wires. Bank
j's circuit does not touch the control bitX iunlessi = n-1-j(its own).The control register is injective on
[0, n).
Instances For
A wire not touched by bank j survives bank j's Horner step. (The Touches predicate lists
exactly the 18 families hornerStep_preserves_external requires disjointness from.)
A clean wire of bank k survives bank j's Horner step (j β k, both < n): by hInter it is
not touched, so notTouches_preserved applies. The 14 Clean-membership constructors are unfolded at
each call site (the mulLoop_invariant step transports bank k's preconditions through bank j).
The Horner arithmetic step. Folding one MSB-first Horner digit through the running residue:
(2Β·((HΒ·Y) mod N) + [bit]Β·Y) mod N = ((2Β·H + [bit])Β·Y) mod N, i.e. the value hornerStep_correct
produces from c = (HΒ·Y) mod N is (hornerVal-next Β· Y) mod N. (Nat.add_mod / Nat.mul_mod absorb
the inner reduction; the if is the 0/1 digit.)
The general-n multiply loop and its correctness #
The general-n modular-multiply loop. Process the multiplier bits MSB-first: bank j
(j = 0, β¦, n-1) runs one verified Horner step acc β (2Β·acc + [X_{n-1-j}]Β·Y) mod N. The banks share
the accumulator B and the multiplicand Y; every other wire is fresh.
Equations
- Reversible.mulLoop L = List.flatMap id (List.map (fun (j : β) => Reversible.hornerStep (L.bank j)) (List.range n))
Instances For
The first k banks of the loop (prefix [0, β¦, k-1]), the induction handle for mulLoop.
Equations
- Reversible.mulLoopUpto L k = List.flatMap id (List.map (fun (j : β) => Reversible.hornerStep (L.bank j)) (List.range k))
Instances For
Split the prefix at its last bank: mulLoopUpto L (k+1) = mulLoopUpto L k ++ hornerStep (bank k)
(bank k runs LAST). From List.range_succ.
Prefix frame. A wire not touched by any bank j < k survives the whole prefix
mulLoopUpto L k (fold of notTouches_preserved).
Preservation of bank k's preconditions through the prefix #
Every clean / preset wire of bank k lies in Clean L.bank k, so (for k β€ n and the prefix length
β€ k) it is untouched by every earlier bank j < k (hInter) and survives mulLoopUpto. The control
bit X (n-1-k) survives via hCtrlTouch. These are the lemmas the invariant's step consumes.
The multiply-loop invariant. After the first k banks (k β€ n), the accumulator holds
(hornerVal bits n k Β· Yval) mod N (bits i = [X i]) and the multiplicand still holds Yval. By
induction on k, splitting the last bank (mulLoopUpto_succ): the prefix preserves bank k's clean /
preset wires (clean_pres) and its control bit (hCtrlTouch); hornerStep_correct then advances the
residue (horner_mod_step), hornerStep_preserves_Y keeps Yval.
The verified general-n modular field multiply (the S6.3d-2b headline). Under the accumulator
initialised 0, the multiplicand Y holding Yval < N, 2N β€ 2βΏ, 0 < N, and every bank's
clean / preset wires set (carries / ancilla false, presets A1 = 2βΏ β N, A2 = N), the loop leaves
the accumulator holding (X Β· Yval) mod N, with the multiplier X arbitrary:
regValRange B (denote (mulLoop L) s) n = (regValRange X s n Β· Yval) % N.
Proof: mulLoop_invariant at k = n gives (hornerVal bits n n Β· Yval) % N; hornerVal_full /
regValRange_eq_hornerVal_bits bridges hornerVal bits n n = regValRange X s n (top n bits of an
n-bit number is itself).
Derived cost #
Derived Toffoli cost of the general-n modular multiply: 30 Β· nΒ² Toffolis. The loop is n
Horner steps (hornerStep_toffoli, 30n each), composed through the fold; multiplier_toffoli turns the
concatenation cost into the sum of the per-step counts, which is n Β· 30n = 30nΒ².
Honest reading: this is the Ξ(nΒ²)-Toffoli, Ξ(nΒ²)-qubit fresh-ancilla figure β each of the n steps
uses its own fresh scratch / carries / ancilla. The optimised in-place (Ξ(n)-qubit) version needs the
carry-clean / ancilla-restoring adder the corpus does not yet provide.
Concrete witness: a 3-bank (n = 3) modular multiply on Fin 135 #
A genuine MulLoopLayout 135 3, exhibiting MulLoopLayout is inhabited and mulLoop_correct applies.
Shared accumulator B β {0,1,2}, multiplicand Y β {3,4,5}, 3-bit multiplier X β {6,7,8} (bit j
on wire 6+j, so bank j reads X (2-j) = wire 6+(2-j) = the high-to-low MSB-first order). Each
bank j owns the disjoint fresh block [9 + 42Β·j, 9 + 42Β·(j+1)) of Fin 135. All disjointness /
injectivity is linear-arithmetic on the wire indices (omega), so the schema is manifestly inhabitable;
the same stride formula inhabits every n (only the ambient Fin m grows).
Bank j's doubling sub-layout on Fin 135: private block [base, base+22) with base = 9+42Β·j.
All wire families are β¨base + offset + min i width, _β©, so every geometry field is omega.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Interface lemmas (CONVENTIONS Β§9.1, F1): the doubling sub-layout's wire assignments at the value level, one lemma per bundled field.
Bank j's controlled-add sub-layout on Fin 135: operand Y β {3,4,5}, control X (2-j) =
wire 6 + (2-j), private block [base+22, base+42) with base = 9+42Β·j.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Interface lemmas (CONVENTIONS Β§9.1, F1): the controlled-add sub-layout's wire assignments at the value level, one lemma per field.
Bank j (j < 3) as a HornerStepLayout 135 3: doubling block wDbl, controlled-add block wAdd
(control X (2-j) = wire 6 + (2-j)), sharing B β {0,1,2}. The 70 cross-disjointness fields are all
linear-arithmetic on the block offsets (omega).
Equations
- One or more equations did not get rendered due to their size.
Instances For
The concrete 3-bank (n = 3) modular-multiply loop layout on Fin 135. B β {0,1,2},
Y β {3,4,5}, X β {6,7,8} (bit i on wire 6 + i), bank j on the fresh block
[9 + 42Β·j, 9 + 42Β·(j+1)). The geometry fields reduce to the block-range lemmas
wBank_clean_range / wBank_touch_range and omega.
Equations
Instances For
Concrete #eval cross-check: the verified n = 3, N = 3 modular multiply on Fin 135 #
wState sets the shared B β {0,1,2} to 0, the multiplicand Y β {3,4,5} and the multiplier
X β {6,7,8} to the given bits, and presets every bank's reduce constants A1 = 2Β³ β 3 = 5 and
A2 = 3 (the 24 preset wires); every scratch / carry / ancilla is false. (N = 3 is forced by the
modular reducer's 2N β€ 2βΏ, i.e. N β€ 4 at n = 3; the multiplicand satisfies Y < N = 3.) Reading
register B (low 3 bits) off the strict Array Bool evaluator (runArr, via the proven bridge
regValRangeArr_eq) gives the value mulLoop_correct constrains, computed instantly. The three
witnesses below realise X Β· Y mod 3 for Y = 2: X = 3 β¦ 6 mod 3 = 0; X = 4 β¦ 8 mod 3 = 2;
X = 5 β¦ 10 mod 3 = 1.