From 16402004d411e15a273d4e6946388d5a93061778 Mon Sep 17 00:00:00 2001 From: Nick Petalas Date: Fri, 23 Oct 2026 10:11:08 +0100 Subject: [PATCH] feat(research): prepare the CRT cache with one inversion --- research/crt-one-inverse-cache/README.md | 201 ++++++++++++++++++++ research/crt-one-inverse-cache/SOURCES.json | 30 +++ research/crt-one-inverse-cache/TASK.json | 86 +++++++++ research/crt-one-inverse-cache/VERIFY.json | 34 ++++ research/crt-one-inverse-cache/cache.py | 80 ++++++++ research/crt-one-inverse-cache/verify.py | 139 ++++++++++++++ 6 files changed, 570 insertions(+) create mode 100644 research/crt-one-inverse-cache/README.md create mode 100644 research/crt-one-inverse-cache/SOURCES.json create mode 100644 research/crt-one-inverse-cache/TASK.json create mode 100644 research/crt-one-inverse-cache/VERIFY.json create mode 100644 research/crt-one-inverse-cache/cache.py create mode 100644 research/crt-one-inverse-cache/verify.py diff --git a/research/crt-one-inverse-cache/README.md b/research/crt-one-inverse-cache/README.md new file mode 100644 index 0000000000..d919370ee7 --- /dev/null +++ b/research/crt-one-inverse-cache/README.md @@ -0,0 +1,201 @@ +# One modular inversion prepares the fixed CRT cache + +**Scoped algebraic construction, no new kappa.** One modular-inverse +provider call prepares all the fixed cofactor inverses and the target +inverse in Hiroki KUMAZAKI's [PR #4716](https://github.com/CrocSwap/integer-mult-bounds/pull/4716) +cache. The cited construction uses `m+1` calls for a left group of size `m`. +The new construction uses one call modulo the full left-and-target product. +For the first source fixture, this changes three inverse calls modulo +`11,13,17` to one inverse call modulo `2431`. + +The inversion modulus is wider. **No bit-cost or runtime improvement is +claimed.** Current-state reconstruction, padded invalidity and the target +offset remain the same mathematical contract. Every subsequent evaluation +uses no inverse call. The three current-state query sites established by +#4716 are unchanged; the cache stores none of their evaluated values. + +## Fixed data and construction + +Let `m>=1`, with pairwise coprime integers `n_0,...,n_(m-1),s>=2` and +positive integer `Q` satisfying `gcd(Q,s)=1`. These include #4716's +left-group moduli, target modulus and preceding right-child product. +Define + +\[ +S=\prod_j n_j,\quad C_j=S/n_j,\quad T=Ss,\quad +A=\left(\sum_j C_j\right)\bmod S,\quad v=SQ\bmod s, +\] + +\[ +U=(sA+Sv)\bmod T,\qquad r=U^{-1}\bmod T.\tag{1} +\] + +This is the sole inverse call. Extract + +\[ +\alpha_j=sr\bmod n_j,\qquad \mu=Sr\bmod s.\tag{2} +\] + +**Theorem.** The unit `U` in (1) is invertible modulo `T`, and (2) gives +`alpha_j=C_j^-1 mod n_j` and `mu=(SQ)^-1 mod s` simultaneously. The claim +holds for composite moduli too; primality is not required. + +For each left modulus, `C_k` is divisible by `n_j` when `k!=j`, so +`U=sC_j mod n_j`. This is a unit. At the target, `U=Sv=S^2Q mod s`, +also a unit. The moduli are pairwise coprime, so `gcd(U,T)=1`. +Reducing `Ur=1` modulo `n_j` gives `C_j(sr)=1`; reducing it modulo `s` +gives `(SQ)(Sr)=1`. These are exactly (2). + +The same packed inverse supplies a domain check. For positive moduli at +least two and positive `Q`, `gcd(U,T)=1` holds **if and only if** the left +and target moduli are pairwise coprime and `gcd(Q,s)=1`. If a prime divides +two left moduli, it divides every `C_j`, hence `A`, `S` and `U`. If it +divides a left modulus and `s`, it divides both summands of `U`. If it +divides `s` and `Q`, then `U=S^2Q=0` modulo that prime. These exhaust the +ways the stated coprimality conditions can fail. The constructive argument +above proves the converse. [cache.py](cache.py) verifies the returned +canonical inverse by `Ur mod T == 1`, so an incorrect provider cannot +silently produce a cache. + +## Preserve the current-state contract + +Let `P_j=product_(k=n_j`, the evaluator returns invalid with `k=c=0`, retaining +the predecessor's disabling rule on padded coordinates. Each call reads +its supplied current vector afresh. Setup depends on no address or payload. + +The new contribution is the packed setup (1), extraction (2), domain +criterion and executable cache construction. The CRT reconstruction +contract, three-epoch distinction and exceptional repair correctness are +credited premises from #4716 and its predecessors. No predecessor +executable, word census or physical caller is replayed here. + +## Exact first fixture and operand bounds + +For `(n_0,n_1;s;Q)=(11,13;17;1)`, the verifier emits + +```text +S=143, C=(13,11), A=24, v=7, T=2431 +U=1409, r=892, alpha=(6,6), mu=5 +``` + +All extraction identities are checked directly as integer congruences. +The artifact makes exactly one provider invocation in setup; its spy +rejects a second invocation, and is retained through all current-vector +evaluations. + +If every fixed modulus has at most `ell` bits, then +`T<2^((m+1)ell)`. Thus the new inverse accepts at most `(m+1)ell` bits, +whereas the old inverses each use at most `ell` bits. This is an explicit +operand-width tradeoff. For `L=bit_length(T)`, `0<=A None: + if not condition: + raise ValueError(message) + + +def inverse_mod(value: int, modulus: int) -> int: + return pow(value, -1, modulus) + + +@dataclass(frozen=True) +class Cache: + moduli: tuple[int, ...] + target: int + product: int + full_modulus: int + unit: int + unit_inverse: int + coefficients: tuple[int, ...] + weights: tuple[int, ...] + target_inverse: int + + def evaluate(self, residues: tuple[int, ...]) -> tuple[bool, int, int]: + """Evaluate the current vector; invalid padded coordinates disable it.""" + require(type(residues) is tuple and len(residues) == len(self.moduli), + "one current residue per left modulus is required") + require(all(type(value) is int and value >= 0 for value in residues), + "current residues must be nonnegative integers") + if any(value >= modulus for value, modulus in zip(residues, self.moduli)): + return False, 0, 0 + integer = sum(value * weight for value, weight in zip(residues, self.weights)) + integer %= self.product + return True, integer, integer * self.target_inverse % self.target + + +def prepare(moduli: tuple[int, ...], target: int, right_prefix: int = 1, + inverse: Callable[[int, int], int] = inverse_mod) -> Cache: + """Use one provider call, modulo target * product(moduli). + + The provider receives canonical arguments and must return a canonical + inverse. One modular identity authenticates its result and simultaneously + rejects noncoprime fixed data. No address or payload enters setup. + """ + require(type(moduli) is tuple and len(moduli) >= 1, + "a nonempty tuple of left moduli is required") + require(all(type(value) is int and value >= 2 for value in moduli), + "left moduli must be integers at least two") + require(type(target) is int and target >= 2, "target must be at least two") + require(type(right_prefix) is int and right_prefix >= 1, + "right prefix must be a positive integer") + require(callable(inverse), "inverse provider must be callable") + product = prod(moduli) + full_modulus = product * target + cofactors = tuple(product // modulus for modulus in moduli) + cofactor_sum = sum(cofactors) % product + target_residue = (product % target) * (right_prefix % target) % target + unit = (target * cofactor_sum + product * target_residue) % full_modulus + unit_inverse = inverse(unit, full_modulus) + require(type(unit_inverse) is int and 0 <= unit_inverse < full_modulus, + "provider must return a canonical integer inverse") + require(unit * unit_inverse % full_modulus == 1, "invalid inverse or fixed data") + common = target * unit_inverse % product + coefficients = tuple(common % modulus for modulus in moduli) + target_inverse = product * (unit_inverse % target) % target + weights = [] + prefix = 1 + for modulus, cofactor, coefficient in zip(moduli, cofactors, coefficients): + require(cofactor * coefficient % modulus == 1, "cofactor inverse failed") + weights.append(prefix * cofactor * coefficient % product) + prefix *= modulus + require(target_residue * target_inverse % target == 1, + "target inverse failed") + return Cache(moduli, target, product, full_modulus, unit, unit_inverse, + coefficients, tuple(weights), target_inverse) diff --git a/research/crt-one-inverse-cache/verify.py b/research/crt-one-inverse-cache/verify.py new file mode 100644 index 0000000000..c3c96b7ad1 --- /dev/null +++ b/research/crt-one-inverse-cache/verify.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Verify the new cache directly from congruences, without predecessor replay.""" + +from itertools import permutations, product +from math import gcd, prod +from pathlib import Path +import hashlib +import json +import runpy + + +API = runpy.run_path(str(Path(__file__).with_name("cache.py"))) +prepare = API["prepare"] +require = API["require"] + + +class InverseSpy: + def __init__(self) -> None: + self.calls = [] + + def __call__(self, value: int, modulus: int) -> int: + require(not self.calls, "second inverse-provider call") + self.calls.append((value, modulus)) + return pow(value, -1, modulus) + + +def oracle_integer(residues: tuple, moduli: tuple) -> int: + """Small bounded oracle: find the unique number by direct enumeration.""" + scaled = [] + prefix = 1 + for residue, modulus in zip(residues, moduli): + scaled.append(residue * prefix % modulus) + prefix *= modulus + matches = [value for value in range(prod(moduli)) + if all(value % modulus == residue + for modulus, residue in zip(moduli, scaled))] + require(len(matches) == 1, "CRT oracle did not have a unique result") + return matches[0] + + +def check_case(moduli: tuple, target: int, prefix: int, exhaustive: bool) -> dict: + spy = InverseSpy() + cache = prepare(moduli, target, prefix, spy) + require(len(spy.calls) == 1, "setup must make exactly one inverse call") + require(spy.calls[0] == (cache.unit, cache.full_modulus), "wrong provider arguments") + require(cache.full_modulus == prod(moduli) * target, "wrong combined modulus") + for modulus, coefficient in zip(moduli, cache.coefficients): + require((cache.product // modulus) * coefficient % modulus == 1, + "extracted coefficient does not solve its congruence") + require(cache.product * prefix * cache.target_inverse % target == 1, + "extracted target inverse does not solve its congruence") + vectors = product(*(range(modulus + 1) for modulus in moduli)) if exhaustive else ( + tuple(0 for _ in moduli), tuple(modulus - 1 for modulus in moduli), + tuple(moduli)) + count = 0 + for residues in vectors: + count += 1 + valid, integer, offset = cache.evaluate(residues) + expected_valid = all(value < modulus for value, modulus in zip(residues, moduli)) + require(valid == expected_valid, "padded validity changed") + if not valid: + require((integer, offset) == (0, 0), "invalid vector must disable offset") + continue + if exhaustive: + require(integer == oracle_integer(residues, moduli), "reconstruction changed") + preceding = 1 + for residue, modulus in zip(residues, moduli): + require(integer % modulus == residue * preceding % modulus, + "locally scaled congruence failed") + preceding *= modulus + require(offset * cache.product * prefix % target == integer % target, + "target offset changed") + require(len(spy.calls) == 1, "evaluation must make no inverse calls") + return {"vectors": count, "modulus_bits": cache.full_modulus.bit_length()} + + +def expect_rejection(operation) -> None: + try: + operation() + except (TypeError, ValueError): + return + raise ValueError("negative control was accepted") + + +def check_controls() -> dict: + cases = [((), 5, 1), ((True,), 5, 1), ((1,), 5, 1), ((3,), True, 1), + ((3,), 5, 0), ((6, 10), 7, 1), ((3, 5), 15, 1), ((3, 5), 7, 7)] + for moduli, target, prefix in cases: + expect_rejection(lambda: prepare(moduli, target, prefix)) + for bad in (True, -1, 0, 999999): + expect_rejection(lambda: prepare((3, 5), 7, 1, lambda u, m: bad)) + cache = prepare((3, 5), 7) + for residues in ((1,), (True, 1), (-1, 0), [0, 0]): + expect_rejection(lambda: cache.evaluate(residues)) + # Omitting the target summand leaves the packed unit divisible by target. + packed_without_target = 7 * ((5 + 3) % 15) % 105 + require(gcd(packed_without_target, 105) == 7, "target-omission control failed") + # Both extraction multipliers are necessary, even in the first source fixture. + fixture = prepare((11, 13), 17) + require(any(fixture.unit_inverse % n != a + for n, a in zip(fixture.moduli, fixture.coefficients)), + "left-multiplier omission control failed") + require(fixture.unit_inverse % 17 != fixture.target_inverse, + "target-multiplier omission control failed") + return {"rejected_inputs_or_providers": len(cases) + 4 + 4, + "semantic_omissions_detected": 3} + + +def main() -> None: + cases = [] + for triple in permutations((2, 3, 4, 5, 7, 8, 9), 3): + if any(gcd(a, b) != 1 for a, b in ((triple[0], triple[1]), + (triple[0], triple[2]), + (triple[1], triple[2]))): + continue + for prefix in (1, 11): + cases.append(check_case(triple[:2], triple[2], prefix, True)) + for moduli, target, prefix in (((11, 13), 17, 1), ((4, 9, 25), 49, 11), + ((3,), 5, 1), ((3, 4, 5, 7), 11, 13), + ((2**127 - 1,), 2**61 - 1, 2**521 - 1)): + cases.append(check_case(moduli, target, prefix, False)) + fixture = prepare((11, 13), 17) + controls = check_controls() + hashes = {name: hashlib.sha256(Path(__file__).with_name(name).read_bytes()).hexdigest() + for name in ("cache.py", "verify.py")} + print(json.dumps({"status": "PASS_CRT_ONE_INVERSE_CACHE", "cases": len(cases), + "vectors": sum(case["vectors"] for case in cases), + "max_inversion_modulus_bits": max(case["modulus_bits"] for case in cases), + "controls": controls, + "source_fixture": {"unit": fixture.unit, + "inverse_modulus": fixture.full_modulus, + "inverse": fixture.unit_inverse, + "alpha": fixture.coefficients, + "mu": fixture.target_inverse}, + "sha256": hashes}, sort_keys=True)) + + +if __name__ == "__main__": + main()