Reversible modular constant-multiply and negation — the last two field-op gadgets (ECDLP Phase 2, Stage S6.3e-2a) #
Category: 1-Mathlib (CSD-free; staged as a Mathlib-upstream candidate).
This module verifies the last two modular field-operation gadgets the SLP → circuit router needs,
completing the toolkit {modAdd, modSub, modDouble, mulLoop}:
modConstMul(c · a mod N, withca CLASSICAL constant): repeated modular addition. Fromacc = 0and a fixed operand registerAop = a, runcsuccessivemodAdd (Aop = a) → (B = acc)on fresh per-step banks; each mapsacc ← (a + acc) mod N(modAdd_correct), preservinga(modAdd_preserves_operand) andacc < N(modAdd_in_range). Aftercstepsacc = (c·a) mod N.modNeg((N − b) mod N, the additive inverse(−b) mod N): exactlymodSubwith the minuend register holding0.modSub_correctata := 0gives(0 + N − b) % N = (N − b) % N.
modConstMul L c = ((List.range c).map (fun j => modAdd (L.bank j))).flatMap id
modNeg L = modSub L -- with the minuend register init 0
Carve line (what this is, and is NOT) #
These are the value-correct field-operation PRIMITIVES in the fresh-ancilla model, completing the
gadget set. ℕ / mod N bit arithmetic; NO field / group / curve semantics here.
modConstMulis repeatedmodAdd: anO(c)-add schedule. An optional cost optimisation is the double-and-add variant (O(log c)modular doublings + adds), which is the standard speedup; it is NOT built here, and is not needed for the SMALL EC coefficients (thensmulcoefficients the EC point op uses are constants≤ 8, which do not even count toward the free parameterM).modNegismodSubwith a zero minuend.(N − b) % Nis0whenb = 0andN − bwhen0 < b < N— the genuine additive inverse(−b) mod N.
Named residue (same fresh-ancilla model as modAdd / modSub): the per-bank carry chains and the
borrow chain / comparison flags are left dirty; correctness holds because each bank / use supplies
fresh wires (the Cadd / C1 / C2 / anc of each modConstMul bank, the Bor / C / anc of modNeg,
are required false). In-place reuse needs carry-clean / ancilla-restoring adders the corpus does NOT
yet provide. The SLP → circuit assembly of the EC point operation (routing ALL opcodes — add, sub,
mul / sq, nsmul, neg — and deriving M as an exhibited-circuit count) is S6.3e-2b / S6.3e-3,
NOT claimed here. This module supplies the two missing opcode gadgets.
Honest cost #
modConstMul_toffoliderivesc · 12 · nToffolis:cmodular adds, each12n(modularAdd_toffoli), composed throughcost_comp_toffoli_countover the fold.modNeg_toffoliis10n, inherited verbatim frommodSub_toffoli.
Part 1 — modConstMul (c · a mod N, repeated modular addition) #
A ConstMulLayout bundles c per-step ModAddLayout banks (bank j, S6.3b), all sharing the operand
register Aop (the fixed addend a) and the accumulator B, with bank j supplying its OWN fresh
Cadd / A1 / C1 / A2 / C2 / anc. The inter-bank geometry — bank j must not touch bank k's clean /
preset wires (k ≠ j) — is carried by the membership footprints CTouches / CClean and the single
disjointness field hInter, mirroring mulLoop's Touches / Clean schema but SIMPLER: no control
bit, no doubling, and the operand Aop is FIXED (shared) across every bank.
The wire families bank k's modAdd_correct reads as a clean / preset precondition: the carry
chains Cadd / C1 / C2 (false), the ancilla anc (false), and the constant presets A1 / A2.
(The shared Aop and B are handled separately by the running invariant.)
Equations
- One or more equations did not get rendered due to their size.
Instances For
A c-bank constant-multiply layout on Fin m. The c banks share the operand Aop and the
accumulator B; bank j does not touch bank k's clean wires for k ≠ j (hInter). All hypotheses
are bounded, so the schema is inhabitable for every c (the witness assigns each bank a disjoint
contiguous block; see constMulLayout2).
- bank : ℕ → ModAddLayout m n
The
cper-step modular-addition banks (bank jruns thej-thacc ← (a + acc) mod N). - hInter (j k : ℕ) (w : Fin m) : j < c → k < c → j ≠ k → CClean self.bank k w → ¬CTouches self.bank j w
Inter-bank disjointness: for
j ≠ k, bankj's circuit does not touch bankk's clean wires.
Instances For
A wire not touched by bank j survives bank j's modular add. (The CTouches predicate lists
exactly the 8 families modAdd_preserves_external requires disjointness from.)
A clean wire of bank k survives bank j's modular add (j ≠ k, both < c): by hInter it is
not touched, so notCTouches_preserved applies.
The constant-multiply circuit and its prefix #
The modular constant-multiply circuit. Run c successive modular adds acc ← (a + acc) mod N,
one per bank, on the shared operand Aop = a and accumulator B = acc; every other wire is fresh.
Equations
- Reversible.constMulCirc L = List.flatMap id (List.map (fun (j : ℕ) => Reversible.modAdd (L.bank j)) (List.range c))
Instances For
The first k banks of the constant-multiply (prefix [0, …, k-1]), the induction handle.
Equations
- Reversible.constMulUpto L k = List.flatMap id (List.map (fun (j : ℕ) => Reversible.modAdd (L.bank j)) (List.range k))
Instances For
Split the prefix at its last bank: constMulUpto L (k+1) = constMulUpto L k ++ modAdd (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
constMulUpto L k (fold of notCTouches_preserved).
A clean / preset wire of bank k survives every earlier-bank prefix (p ≤ k < c): each lies in
CClean L.bank k, so hInter keeps it untouched by every bank j < p ≤ k.
The constant-multiply invariant and correctness #
The constant-multiply invariant. After the first k banks (k ≤ c), from acc = 0 the
accumulator holds (k · aval) mod N and the operand still holds aval. By induction on k, splitting
the last bank (constMulUpto_succ): the prefix preserves bank k's clean / preset wires (cclean_pres)
and the running Aop/B; modAdd_correct then maps acc = (k·aval) mod N to
(aval + (k·aval) mod N) mod N = ((k+1)·aval) mod N (Nat.add_mod), and modAdd_preserves_operand
keeps aval.
The verified modular constant-multiply (the S6.3e-2a Part-1 headline). Under the accumulator
initialised 0, the operand Aop holding aval < N, 2N ≤ 2ⁿ, and every bank's clean / preset
wires set (carries / ancilla false, presets A1 = 2ⁿ − N, A2 = N), the circuit leaves the
accumulator holding (c · aval) mod N:
regValRange B (denote (constMulCirc L) s) n = (c · aval) % N.
Proof: constMul_invariant at k = c. Both branches — the c = 0 base (acc = 0 = (0·aval)%N) and
the c → c+1 step ((aval + (k·aval)%N)%N = ((k+1)·aval)%N) — are genuinely covered by the
induction.
The operand register is intact. modConstMul leaves Aop holding aval (read-only addend,
which the SLP may reuse). Read off the invariant's second clause at k = c.
The constant-multiply output is a genuine residue in [0, N). Corollary of
modConstMul_correct and Nat.mod_lt.
Derived cost #
Derived Toffoli cost of the modular constant-multiply: c · 12 · n Toffolis, from the
exhibited gate list. The circuit is c modular adds (modularAdd_toffoli, 12n each), composed
through the fold; the concatenation cost is the sum of the per-bank counts, c · 12n.
Honest reading: O(c) adds (fresh-ancilla model). The O(log c) double-and-add variant is the
standard speedup but is NOT built here (and is not needed for the small EC nsmul coefficients).
Non-vacuity witness: a 3-bank (c = 3) constant-multiply on Fin 200 #
A genuine ConstMulLayout 200 4 3, exhibiting that ConstMulLayout is inhabited and
modConstMul_correct applies. Shared operand Aop → {0,1,2,3}, shared accumulator B → {4,5,6,7};
each bank j owns the disjoint fresh block [8 + 24·j, 8 + 24·(j+1)) of Fin 200 for its private
Cadd / A1 / C1 / A2 / C2 / anc. All disjointness is linear arithmetic on the wire indices (omega),
so the schema is manifestly inhabitable; the same stride formula inhabits every c.
n = 4 is needed (not n = 3): the modular adder requires 2N ≤ 2ⁿ so the add does not wrap; for
N = 5 that forces 2ⁿ ≥ 10, i.e. n ≥ 4. The registers (Aop, B, A1, A2) are 4-bit (min i 3);
the carry chains (Cadd, C1, C2) are 5-element (min i 4).
Bank j's ModAddLayout 200 4 on the shared Aop → {0,1,2,3}, B → {4,5,6,7} and the private
block [base, base+24) with base = 8 + 24·j: Cadd → [base,base+5), A1 → [base+5,base+9),
C1 → [base+9,base+14), A2 → [base+14,base+18), C2 → [base+18,base+23), anc → base+23.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Interface lemmas (CONVENTIONS §9.1, F1): the bank's wire assignments at the value level, one lemma per field — what the range/interleaving proofs project out of the structure.
The concrete 3-bank (c = 3) constant-multiply layout on Fin 200. Aop → {0,1,2,3},
B → {4,5,6,7}, bank j on the fresh block [8 + 24·j, 8 + 24·(j+1)). The geometry fields reduce to
the block-range lemmas cBank_clean_range / cBank_touch_range and omega.
Equations
- Reversible.constMulLayout2 = { bank := fun (j : ℕ) => Reversible.cBank (8 + 24 * min j 2) ⋯ ⋯, hAopShare := ⋯, hBShare := ⋯, hInter := ⋯ }
Instances For
Every bank of constMulLayout2 is the standard cBank at its stride-24 base
(interface lemma, §9.1).
Harness #eval cross-checks and proven instances (c = 3, 8, 0, 1) #
constMulState sets the shared Aop → {0,1,2,3} to aval, the accumulator B → {4,5,6,7} to 0, and
presets every bank's reduce constants A1 = 2ⁿ − N = 11 (bits 0,1,3) and A2 = N = 5 (bits 0,2);
every scratch / carry / ancilla is false. Reading register B (low 4 bits) off the strict
Array Bool evaluator (runArr, via the proven bridge regValRangeArr_eq) gives the value
modConstMul_correct constrains, computed instantly. N = 5 (2N = 10 ≤ 2⁴) throughout.
Concrete input state on Fin 200 for c, n = 4, N = 5: Aop = (a0,a1,a2) on wires {0,1,2}
(top bit Aop 3 = 0), B = 0 (wires {4,5,6,7}), every bank's presets A1 = 11 (block bits 0,1,3)
and A2 = 5 (block bits 0,2), all scratch / carries / ancilla false. Bank j's A1 is at wires
{base+5, base+6, base+8}, A2 at {base+14, base+16} with base = 8 + 24·j.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The clean / preset preconditions of modConstMul_correct hold at constMulState, for any operand
bits (n = 4, N = 5). The carry / ancilla families are false; the presets A1 = 11, A2 = 5.
Discharged by interval_cases over the 3 banks + cmin4_cases over the wire index.
Proven instance, c = 1: c = 1, a = 3, N = 5 ↦ (1 · 3) mod 5 = 3 (a single add). Uses a
ConstMulLayout 200 4 1 — the same banks, c = 1.
Equations
- Reversible.constMulLayout1 = { bank := fun (j : ℕ) => Reversible.cBank (8 + 24 * min j 2) ⋯ ⋯, hAopShare := ⋯, hBShare := ⋯, hInter := ⋯ }
Instances For
Every bank of constMulLayout1 is the standard cBank at its stride-24 base
(interface lemma, §9.1).
Proven instance, the c = 0 base case: c = 0 ↦ 0 (empty circuit, accumulator unchanged at
0). Uses a ConstMulLayout 200 4 0; the headline lands on (0 · aval) % N = 0 with NO add.
Equations
- Reversible.constMulLayout0 = { bank := fun (j : ℕ) => Reversible.cBank (8 + 24 * min j 2) ⋯ ⋯, hAopShare := ⋯, hBShare := ⋯, hInter := ⋯ }
Instances For
Every bank of constMulLayout0 is the standard cBank at its stride-24 base
(interface lemma, §9.1).
The c = 8 wrap witness (harness cross-check): c = 8, a = 2, N = 5 ↦ 16 mod 5 = 1. Needs a
ConstMulLayout 200 4 8; the strided block formula inhabits c = 8 (8 + 24·7 + 24 = 200). The
#eval prints 1, the genuinely-wrapped residue (16 = 3·5 + 1), confirming the running acc < N
maintenance across all 8 adds.
Equations
- Reversible.constMulLayout8 = { bank := fun (j : ℕ) => Reversible.cBank (8 + 24 * min j 7) ⋯ ⋯, hAopShare := ⋯, hBShare := ⋯, hInter := ⋯ }
Instances For
Every bank of constMulLayout8 is the standard cBank at its stride-24 base
(interface lemma, §9.1).
constMulState8 presets all 8 banks' A1 = 11 and A2 = 5 (blocks base = 8 + 24·j, j < 8),
Aop = a, B = 0. Bank j's A1 is {base+5, base+6, base+8}, A2 is {base+14, base+16}.
Equations
- One or more equations did not get rendered due to their size.
Instances For
Part 2 — modNeg ((N − b) mod N, the additive inverse) #
modNeg(b) = (N − b) mod N = (0 − b) mod N, which is exactly modSub with the minuend register B
holding 0. So modNeg IS modSub; the corollary modNeg_correct instantiates modSub_correct at
a := 0. (N − b) % N is 0 when b = 0 and N − b when 0 < b < N — the genuine additive inverse
(−b) mod N.
The modular-negation circuit. Definitionally modSub (run with the minuend register B
initialised to 0, so the output is (N − b) mod N).
Equations
Instances For
The verified modular negation (the S6.3e-2a Part-2 headline). For a disjoint-wire
ModSubLayout with the borrow chain Bor, the fix carry chain C, and the ancilla anc initialised
false, the constant register Nreg preset to N, the minuend B holding 0, the subtrahend
Sub holding b, with b < N, N ≤ 2ⁿ: the circuit modNeg L leaves B holding (N − b) mod N.
A thin corollary of modSub_correct at a := 0: (0 + N − b) % N = (N − b) % N. Both branches are
covered — b = 0 gives (N − 0) % N = 0 (the borrow-clear branch, a ≥ b), and 0 < b < N gives
(N − b) % N = N − b (the borrow-set WRAP branch).
The modular-negation output is a genuine residue in [0, N). Corollary of modNeg_correct
and Nat.mod_lt.
Derived Toffoli cost of the modular negation: 10n Toffolis, inherited verbatim from
modSub_toffoli (modNeg IS modSub).