use logicaffeine_base::BigInt;
use std::f64::consts::PI;
#[inline]
fn zero() -> BigInt {
BigInt::from_i64(0)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cyclo {
pub n: usize,
pub coeffs: Vec<BigInt>,
}
impl Cyclo {
pub fn new(n: usize, mut coeffs: Vec<BigInt>) -> Cyclo {
assert!(n.is_power_of_two(), "R = ℤ[X]/(Xⁿ+1) requires n a power of two");
assert!(coeffs.len() <= n, "an element of R has degree < n");
coeffs.resize(n, zero());
Cyclo { n, coeffs }
}
pub fn from_ints(n: usize, v: &[i64]) -> Cyclo {
Cyclo::new(n, v.iter().map(|&x| BigInt::from_i64(x)).collect())
}
pub fn zero(n: usize) -> Cyclo {
Cyclo::new(n, vec![])
}
pub fn one(n: usize) -> Cyclo {
Cyclo::from_ints(n, &[1])
}
pub fn monomial(n: usize, i: usize, coeff: BigInt) -> Cyclo {
let mut c = vec![zero(); n];
let signed = if (i / n) % 2 == 0 { coeff } else { zero().sub(&coeff) };
c[i % n] = signed;
Cyclo { n, coeffs: c }
}
pub fn add(&self, o: &Cyclo) -> Cyclo {
Cyclo { n: self.n, coeffs: (0..self.n).map(|i| self.coeffs[i].add(&o.coeffs[i])).collect() }
}
pub fn sub(&self, o: &Cyclo) -> Cyclo {
Cyclo { n: self.n, coeffs: (0..self.n).map(|i| self.coeffs[i].sub(&o.coeffs[i])).collect() }
}
pub fn neg(&self) -> Cyclo {
Cyclo { n: self.n, coeffs: self.coeffs.iter().map(|c| zero().sub(c)).collect() }
}
pub fn mul(&self, o: &Cyclo) -> Cyclo {
let n = self.n;
let mut c = vec![zero(); n];
for i in 0..n {
if self.coeffs[i].is_zero() {
continue;
}
for j in 0..n {
let prod = self.coeffs[i].mul(&o.coeffs[j]);
let k = i + j;
if k < n {
c[k] = c[k].add(&prod); } else {
c[k - n] = c[k - n].sub(&prod); }
}
}
Cyclo { n, coeffs: c }
}
pub fn galois(&self, t: u64) -> Cyclo {
let n = self.n;
let twon = 2 * n as u64;
let t = t % twon;
assert!(t % 2 == 1, "Galois automorphisms σ_t require t odd (a unit mod 2n)");
let mut c = vec![zero(); n];
for i in 0..n {
let m = ((i as u64) * t) % twon;
let (idx, neg) = if (m as usize) < n { (m as usize, false) } else { (m as usize - n, true) };
c[idx] = if neg { c[idx].sub(&self.coeffs[i]) } else { c[idx].add(&self.coeffs[i]) };
}
Cyclo { n, coeffs: c }
}
pub fn conjugate(&self) -> Cyclo {
self.galois((2 * self.n - 1) as u64)
}
fn galois_product(&self) -> Cyclo {
galois_group(self.n).into_iter().fold(Cyclo::one(self.n), |acc, t| acc.mul(&self.galois(t)))
}
pub fn norm(&self) -> BigInt {
self.galois_product().coeffs[0].clone()
}
pub fn trace(&self) -> BigInt {
let sum = galois_group(self.n).into_iter().fold(Cyclo::zero(self.n), |acc, t| acc.add(&self.galois(t)));
sum.coeffs[0].clone()
}
pub fn is_unit(&self) -> bool {
let nrm = self.norm();
nrm == BigInt::from_i64(1) || nrm == BigInt::from_i64(-1)
}
pub fn unit_inverse(&self) -> Option<Cyclo> {
let nrm = self.norm();
let one = BigInt::from_i64(1);
let neg = BigInt::from_i64(-1);
let sign_neg = if nrm == one {
false
} else if nrm == neg {
true
} else {
return None;
};
let prod = galois_group(self.n)
.into_iter()
.filter(|&t| t != 1)
.fold(Cyclo::one(self.n), |acc, t| acc.mul(&self.galois(t)));
Some(if sign_neg { prod.neg() } else { prod })
}
pub fn coeff_norm(&self) -> i64 {
self.coeffs.iter().map(|c| c.to_i64().unwrap_or(i64::MAX / 2).abs()).sum()
}
}
fn embed_at(a: &Cyclo, j: usize) -> (f64, f64) {
let twon = 2.0 * a.n as f64;
let (mut re, mut im) = (0.0f64, 0.0f64);
for (i, c) in a.coeffs.iter().enumerate() {
let ci = c.to_i64().expect("embedding assumes coefficients fit i64") as f64;
let ang = 2.0 * PI * (j as f64) * (i as f64) / twon;
re += ci * ang.cos();
im += ci * ang.sin();
}
(re, im)
}
fn log_embedding(a: &Cyclo) -> Vec<f64> {
(1..2 * a.n)
.step_by(2)
.map(|j| {
let (re, im) = embed_at(a, j);
0.5 * (re * re + im * im).ln()
})
.collect()
}
fn dot(u: &[f64], v: &[f64]) -> f64 {
u.iter().zip(v).map(|(a, b)| a * b).sum()
}
fn solve_linear(mut a: Vec<Vec<f64>>, mut b: Vec<f64>) -> Option<Vec<f64>> {
let n = b.len();
for col in 0..n {
let piv = (col..n).max_by(|&r1, &r2| {
a[r1][col].abs().partial_cmp(&a[r2][col].abs()).unwrap_or(std::cmp::Ordering::Equal)
})?;
if a[piv][col].abs() < 1e-9 {
return None;
}
a.swap(col, piv);
b.swap(col, piv);
for r in 0..n {
if r == col {
continue;
}
let f = a[r][col] / a[col][col];
for k in col..n {
a[r][k] -= f * a[col][k];
}
b[r] -= f * b[col];
}
}
Some((0..n).map(|i| b[i] / a[i][i]).collect())
}
pub fn cyclotomic_units(n: usize) -> Vec<Cyclo> {
(3..n).step_by(2).map(|j| Cyclo::from_ints(n, &vec![1i64; j])).collect()
}
pub fn recover_short_generator(h: &Cyclo) -> Option<Cyclo> {
let n = h.n;
let units = cyclotomic_units(n);
let r = units.len();
let blogs: Vec<Vec<f64>> = units.iter().map(log_embedding).collect();
let target = log_embedding(h);
let mut btb = vec![vec![0.0; r]; r];
let mut bt = vec![0.0; r];
for i in 0..r {
for k in 0..r {
btb[i][k] = dot(&blogs[i], &blogs[k]);
}
bt[i] = dot(&blogs[i], &target);
}
let c = solve_linear(btb, bt)?;
let base: Vec<i64> = c.iter().map(|&x| x.round() as i64).collect();
let inverses: Vec<Cyclo> = units.iter().filter_map(|u| u.unit_inverse()).collect();
if inverses.len() != r {
return None;
}
let mut best: Option<(i64, Cyclo)> = None;
for mask in 0..3usize.pow(r as u32) {
let mut m = mask;
let mut uprime = Cyclo::one(n);
for i in 0..r {
let e = base[i] + (m % 3) as i64 - 1;
m /= 3;
let base_elt = if e >= 0 { &units[i] } else { &inverses[i] };
for _ in 0..e.unsigned_abs() {
uprime = uprime.mul(base_elt);
}
}
let Some(uinv) = uprime.unit_inverse() else { continue };
let g = h.mul(&uinv);
let norm = g.coeff_norm();
if best.as_ref().is_none_or(|(bn, _)| norm < *bn) {
best = Some((norm, g));
}
}
best.map(|(_, g)| g)
}
fn gram_schmidt(vs: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut out: Vec<Vec<f64>> = Vec::new();
for v in vs {
let mut u = v.clone();
for w in &out {
let coef = dot(v, w) / dot(w, w);
for j in 0..u.len() {
u[j] -= coef * w[j];
}
}
out.push(u);
}
out
}
#[derive(Clone, Debug)]
pub struct LogUnitAudit {
pub n: usize,
pub rank: usize,
pub gs_lengths: Vec<f64>,
pub covering_radius_bound: f64,
}
pub fn audit_log_unit(n: usize) -> LogUnitAudit {
let units = cyclotomic_units(n);
let blogs: Vec<Vec<f64>> = units.iter().map(log_embedding).collect();
let gs = gram_schmidt(&blogs);
let gs_lengths: Vec<f64> = gs.iter().map(|v| dot(v, v).sqrt()).collect();
let cov = 0.5 * gs_lengths.iter().map(|l| l * l).sum::<f64>().sqrt();
LogUnitAudit { n, rank: units.len(), gs_lengths, covering_radius_bound: cov }
}
pub fn recovery_margin(g: &Cyclo) -> f64 {
let n = g.n;
let blogs: Vec<Vec<f64>> = cyclotomic_units(n).iter().map(log_embedding).collect();
let r = blogs.len();
let lg = log_embedding(g);
let mut btb = vec![vec![0.0; r]; r];
let mut bt = vec![0.0; r];
for i in 0..r {
for k in 0..r {
btb[i][k] = dot(&blogs[i], &blogs[k]);
}
bt[i] = dot(&blogs[i], &lg);
}
let Some(c) = solve_linear(btb, bt) else { return f64::INFINITY };
let mut proj = vec![0.0; n];
for i in 0..r {
for (j, pj) in proj.iter_mut().enumerate() {
*pj += c[i] * blogs[i][j];
}
}
dot(&proj, &proj).sqrt() / audit_log_unit(n).covering_radius_bound
}
pub fn approximation_scale(n: usize) -> f64 {
let a = audit_log_unit(n);
let shortest = a.gs_lengths.iter().cloned().fold(f64::INFINITY, f64::min);
a.covering_radius_bound / shortest
}
pub fn galois_group(n: usize) -> Vec<u64> {
(1..(2 * n as u64)).step_by(2).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn bi(x: i64) -> BigInt {
BigInt::from_i64(x)
}
#[test]
fn negacyclic_multiplication_reduces_x_to_the_n_as_minus_one() {
let n = 8;
let x = Cyclo::monomial(n, 1, bi(1));
let xnm1 = Cyclo::monomial(n, n - 1, bi(1));
assert_eq!(x.mul(&xnm1), Cyclo::one(n).neg(), "Xⁿ ≡ −1 (negacyclic)");
assert_eq!(Cyclo::monomial(n, n + 2, bi(1)), Cyclo::monomial(n, 2, bi(-1)));
let a = Cyclo::from_ints(n, &[1, -2, 3, 0, -1, 5, 0, 2]);
let b = Cyclo::from_ints(n, &[0, 1, -1, 4, 2, 0, -3, 1]);
let c = Cyclo::from_ints(n, &[2, 0, 1, -1, 0, 3, 1, -2]);
assert_eq!(a.mul(&Cyclo::one(n)), a, "1 is the identity");
assert_eq!(a.mul(&b), b.mul(&a), "multiplication commutes");
assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "distributivity");
assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "associativity");
}
#[test]
fn every_galois_map_is_a_ring_automorphism() {
let n = 8;
let a = Cyclo::from_ints(n, &[1, -2, 3, 0, -1, 5, 0, 2]);
let b = Cyclo::from_ints(n, &[0, 1, -1, 4, 2, 0, -3, 1]);
for &t in &galois_group(n) {
assert_eq!(a.add(&b).galois(t), a.galois(t).add(&b.galois(t)), "σ_{t} preserves addition");
assert_eq!(a.mul(&b).galois(t), a.galois(t).mul(&b.galois(t)), "σ_{t} preserves multiplication");
assert_eq!(Cyclo::one(n).galois(t), Cyclo::one(n), "σ_{t} fixes 1");
}
}
#[test]
fn the_galois_group_is_z_mod_2n_star_of_order_n() {
let n = 8; let group = galois_group(n);
assert_eq!(group.len(), n, "there are exactly φ(2n) = n automorphisms");
let a = Cyclo::from_ints(n, &[3, 1, -2, 0, 4, -1, 2, 5]);
assert_eq!(a.galois(1), a, "σ_1 is the identity");
let twon = (2 * n) as u64;
for &s in &group {
for &t in &group {
assert_eq!(a.galois(t).galois(s), a.galois((s * t) % twon), "σ_s ∘ σ_t = σ_{{st}}");
}
}
let x = Cyclo::monomial(n, 1, bi(1));
let images: Vec<Cyclo> = group.iter().map(|&t| x.galois(t)).collect();
for i in 0..images.len() {
for j in (i + 1)..images.len() {
assert_ne!(images[i], images[j], "distinct t ⟹ distinct automorphism");
}
}
let mut closure = vec![1u64];
let gens = [twon - 1, 3];
loop {
let mut grew = false;
for &g in &gens {
for k in 0..closure.len() {
let p = (closure[k] * g) % twon;
if !closure.contains(&p) {
closure.push(p);
grew = true;
}
}
}
if !grew {
break;
}
}
closure.sort_unstable();
let mut all = group.clone();
all.sort_unstable();
assert_eq!(closure, all, "⟨σ_{{-1}}, σ_3⟩ generates the full Galois group");
}
#[test]
fn conjugation_is_the_order_two_automorphism() {
let n = 8;
let a = Cyclo::from_ints(n, &[3, 1, -2, 0, 4, -1, 2, 5]);
assert_eq!(a.conjugate().conjugate(), a, "σ_{{-1}}² = id");
let x = Cyclo::monomial(n, 1, bi(1));
assert_eq!(x.conjugate(), Cyclo::monomial(n, n - 1, bi(-1)), "σ_{{-1}}(X) = −X^{{n-1}}");
assert_eq!(a.conjugate().coeffs[0], a.coeffs[0], "the rational trace part is fixed");
}
#[test]
fn norm_and_trace_are_rational_integers() {
let n = 8;
let a = Cyclo::from_ints(n, &[3, 1, -2, 0, 4, -1, 2, 5]);
let b = Cyclo::from_ints(n, &[1, 0, 1, -1, 2, 1, 0, -2]);
assert!(a.galois_product().coeffs[1..].iter().all(|c| c.is_zero()), "N(a) is a rational integer");
assert_eq!(a.mul(&b).norm(), a.norm().mul(&b.norm()), "N(ab) = N(a)·N(b)");
assert_eq!(a.add(&b).trace(), a.trace().add(&b.trace()), "Tr(a+b) = Tr(a)+Tr(b)");
assert_eq!(Cyclo::one(n).norm(), bi(1), "N(1) = 1");
assert_eq!(Cyclo::one(n).trace(), bi(n as i64), "Tr(1) = n");
}
#[test]
fn cyclotomic_units_have_norm_plus_minus_one() {
let n = 8;
let u = Cyclo::from_ints(n, &[1, 1, 1]);
assert_eq!(u.norm(), bi(1), "the cyclotomic unit 1+X+X² has norm 1");
assert!(u.is_unit(), "hence it is a unit of R");
for &t in &galois_group(n) {
assert!(u.galois(t).is_unit(), "σ_t(u) is again a unit");
}
let non_unit = Cyclo::from_ints(n, &[1, 1]);
assert_eq!(non_unit.norm(), bi(2), "N(1+X) = 2");
assert!(!non_unit.is_unit(), "1+X is not a unit (it generates the ramified prime above 2)");
}
#[test]
fn unit_inverse_is_an_exact_ring_inverse() {
let n = 8;
let u = Cyclo::from_ints(n, &[1, 1, 1]); let inv = u.unit_inverse().expect("a unit has a ring inverse");
assert_eq!(u.mul(&inv), Cyclo::one(n), "u · u⁻¹ = 1 exactly in R");
assert!(Cyclo::from_ints(n, &[1, 1]).unit_inverse().is_none(), "1+X is not invertible in R");
}
#[test]
fn cdpr_strips_the_unit_and_recovers_the_short_generator() {
let n = 8;
let g = Cyclo::from_ints(n, &[1, -1, 0, 0, 0, 0, 0, 0]); let units = cyclotomic_units(n);
let u = units[0].mul(&units[1]); assert!(u.is_unit(), "the mask is a unit");
let h = g.mul(&u);
assert!(h.coeff_norm() > g.coeff_norm(), "the public generator h is long (unit-masked)");
let rec = recover_short_generator(&h).expect("CDPR recovery succeeds");
assert_eq!(rec.coeff_norm(), g.coeff_norm(), "recovered a generator as short as the secret");
let recn = rec.norm();
assert!(recn == bi(2) || recn == bi(-2), "|N(rec)| = |N(g)| — a genuine generator of the secret ideal");
assert!(h.coeff_norm() >= 2 * rec.coeff_norm(), "and far shorter than the unit-masked public h");
assert!(!rec.is_unit(), "rec generates the secret ideal (norm 2), not R");
}
#[test]
fn the_log_unit_geometry_audit_is_sane_and_the_scale_grows_with_n() {
for n in [8usize, 16, 32] {
let a = audit_log_unit(n);
assert_eq!(a.rank, n / 2 - 1, "unit rank = n/2 − 1 (Dirichlet unit theorem)");
assert!(a.gs_lengths.iter().all(|&l| l > 1e-9), "the cyclotomic-unit log-basis is nondegenerate");
assert!(a.covering_radius_bound > 0.0, "a positive decoding scale");
}
let (m8, m16, m32) = (
audit_log_unit(8).covering_radius_bound,
audit_log_unit(16).covering_radius_bound,
audit_log_unit(32).covering_radius_bound,
);
assert!(m8 < m16 && m16 < m32, "μ(Λ) grows with n: {m8} < {m16} < {m32}");
}
#[test]
fn recovery_margin_locates_the_short_generator_wall() {
let n = 8;
assert!(recovery_margin(&Cyclo::one(n)) < 1e-6, "Log(1) = 0 ⟹ margin 0");
let g = Cyclo::from_ints(n, &[1, -1, 0, 0, 0, 0, 0, 0]);
let m_g = recovery_margin(&g);
assert!(m_g < 1.0, "the short principal-ideal generator is inside the wall (margin {m_g} < 1)");
let u = cyclotomic_units(n)[1].clone();
assert!(recovery_margin(&u) > m_g, "a unit sits farther out along the log-unit directions than g");
}
#[test]
fn the_upstream_wall_the_approximation_scale_grows_with_n() {
let ns = [8usize, 16, 32, 64];
let scales: Vec<f64> = ns.iter().map(|&n| approximation_scale(n)).collect();
for (n, s) in ns.iter().zip(&scales) {
eprintln!("approximation_scale(n={n}) = {s:.4}");
assert!(s.is_finite() && *s > 0.0, "a finite positive decoding scale");
}
assert!(scales[3] > scales[0], "the approximation scale grows with n: {scales:?}");
}
}