From 4eea4c825b737fbf66d2c4ec83b56fae3571884e Mon Sep 17 00:00:00 2001 From: feltroidprime <96737978+feltroidprime@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:16:10 +0200 Subject: [PATCH 1/4] feat: implement Euclidean division for ComplexNumber with symmetric rounding and hex-lattice optimization --- field/eisenstein/eisenstein.go | 71 ++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/field/eisenstein/eisenstein.go b/field/eisenstein/eisenstein.go index e80915413..ab72b7794 100644 --- a/field/eisenstein/eisenstein.go +++ b/field/eisenstein/eisenstein.go @@ -9,6 +9,27 @@ type ComplexNumber struct { A0, A1 *big.Int } +// ────────────────────────────────────────────────────────────────────────────── +// helpers – hex-lattice geometry & symmetric rounding +// ────────────────────────────────────────────────────────────────────────────── + +// six axial directions of the hexagonal lattice +var neighbours = [][2]int64{ + {1, 0}, {0, 1}, {-1, 1}, {-1, 0}, {0, -1}, {1, -1}, +} + +// roundNearest returns ⌊(z + d/2) / d⌋ for *any* sign of z, d>0 +func roundNearest(z, d *big.Int) *big.Int { + half := new(big.Int).Rsh(d, 1) // d / 2 + if z.Sign() >= 0 { + return new(big.Int).Div(new(big.Int).Add(z, half), d) + } + tmp := new(big.Int).Neg(z) + tmp.Add(tmp, half) + tmp.Div(tmp, d) + return tmp.Neg(tmp) +} + func (z *ComplexNumber) init() { if z.A0 == nil { z.A0 = new(big.Int) @@ -124,19 +145,55 @@ func (z *ComplexNumber) Norm() *big.Int { return norm } -// QuoRem sets z to the quotient of x and y, r to the remainder, and returns z and r. +// QuoRem sets z to the Euclidean quotient of x / y, r to the remainder, +// and guarantees ‖r‖ < ‖y‖ (true Euclidean division in ℤ[ω]). func (z *ComplexNumber) QuoRem(x, y, r *ComplexNumber) (*ComplexNumber, *ComplexNumber) { - norm := y.Norm() - if norm.Cmp(big.NewInt(0)) == 0 { + + norm := y.Norm() // > 0 (Eisenstein norm is always non-neg) + if norm.Sign() == 0 { panic("division by zero") } - z.Conjugate(y) - z.Mul(x, z) - z.A0.Div(z.A0, norm) - z.A1.Div(z.A1, norm) + + // num = x * ȳ (ȳ computed in a fresh variable → y unchanged) + var yConj, num ComplexNumber + yConj.Conjugate(y) + num.Mul(x, &yConj) + + // first guess by *symmetric* rounding of both coordinates + q0 := roundNearest(num.A0, norm) + q1 := roundNearest(num.A1, norm) + z.A0, z.A1 = q0, q1 + + // r = x – q*y r.Mul(y, z) r.Sub(x, r) + // If Euclidean inequality already holds we're done. + // Otherwise walk ≤2 unit steps in the hex lattice until N(r) < N(y). + for r.Norm().Cmp(norm) >= 0 { + bestQ0, bestQ1 := new(big.Int).Set(z.A0), new(big.Int).Set(z.A1) + bestR := new(ComplexNumber).Set(r) + bestN2 := bestR.Norm() + + for _, dir := range neighbours { + candQ0 := new(big.Int).Add(z.A0, big.NewInt(dir[0])) + candQ1 := new(big.Int).Add(z.A1, big.NewInt(dir[1])) + var candQ ComplexNumber + candQ.A0, candQ.A1 = candQ0, candQ1 + + var candR ComplexNumber + candR.Mul(y, &candQ) + candR.Sub(x, &candR) + + if candR.Norm().Cmp(bestN2) < 0 { + bestQ0, bestQ1 = candQ0, candQ1 + bestR.Set(&candR) + bestN2 = bestR.Norm() + } + } + z.A0, z.A1 = bestQ0, bestQ1 + r.Set(bestR) // update remainder and retry; Euclidean property ⇒ ≤ 2 loops + } return z, r } From d08de3c06cdfca1d2ca7026c8f8173fe028457ee Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Wed, 30 Apr 2025 23:23:36 +0000 Subject: [PATCH 2/4] test: add Eisenstein integer quorem test --- field/eisenstein/eisenstein_test.go | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/field/eisenstein/eisenstein_test.go b/field/eisenstein/eisenstein_test.go index 6aff795f9..adf71c00c 100644 --- a/field/eisenstein/eisenstein_test.go +++ b/field/eisenstein/eisenstein_test.go @@ -240,6 +240,42 @@ func TestEisensteinHalfGCD(t *testing.T) { properties.TestingRun(t, gopter.ConsoleReporter(false)) } +func TestEisensteinQuoRem(t *testing.T) { + t.Parallel() + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + genE := GenComplexNumber(boundSize) + + properties.Property("QuoRem should be correct", prop.ForAll( + func(a, b *ComplexNumber) bool { + var z, rem ComplexNumber + z.QuoRem(a, b, &rem) + var res ComplexNumber + res.Mul(b, &z) + res.Add(&res, &rem) + return res.Equal(a) + }, + genE, + genE, + )) + + properties.Property("QuoRem remainder should be smaller than divisor", prop.ForAll( + func(a, b *ComplexNumber) bool { + var z, rem ComplexNumber + z.QuoRem(a, b, &rem) + return rem.Norm().Cmp(b.Norm()) == -1 + }, + genE, + genE, + )) +} + // GenNumber generates a random integer func GenNumber(boundSize int64) gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { From bf7a62108a3c4fa3580e4feb952b5aa747ded869 Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Fri, 2 May 2025 11:00:08 +0000 Subject: [PATCH 3/4] test: add regression test for 1483 --- field/eisenstein/eisenstein_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/field/eisenstein/eisenstein_test.go b/field/eisenstein/eisenstein_test.go index adf71c00c..0d4620444 100644 --- a/field/eisenstein/eisenstein_test.go +++ b/field/eisenstein/eisenstein_test.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "math/big" "testing" + "time" "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" @@ -276,6 +277,30 @@ func TestEisensteinQuoRem(t *testing.T) { )) } +func TestRegressionHalfGCD1483(t *testing.T) { + // This test is a regression test for issue #1483 in gnark + a0, _ := new(big.Int).SetString("64502973549206556628585045361533709077", 10) + a1, _ := new(big.Int).SetString("-303414439467246543595250775667605759171", 10) + c0, _ := new(big.Int).SetString("-432420386565659656852420866390673177323", 10) + c1, _ := new(big.Int).SetString("238911465918039986966665730306072050094", 10) + a := ComplexNumber{A0: a0, A1: a1} + c := ComplexNumber{A0: c0, A1: c1} + + ticker := time.NewTimer(time.Second * 3) + doneCh := make(chan struct{}) + go func() { + HalfGCD(&a, &c) + close(doneCh) + }() + + select { + case <-ticker.C: + t.Error("HalfGCD took too long to compute") + case <-doneCh: + // Test passed + } +} + // GenNumber generates a random integer func GenNumber(boundSize int64) gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { From 6cbd90797f3b73386cbe6a1b9a1fcfc61f09811e Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Fri, 2 May 2025 11:00:36 +0000 Subject: [PATCH 4/4] refactor: use if instead of for --- field/eisenstein/eisenstein.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/field/eisenstein/eisenstein.go b/field/eisenstein/eisenstein.go index ab72b7794..033ed902b 100644 --- a/field/eisenstein/eisenstein.go +++ b/field/eisenstein/eisenstein.go @@ -170,7 +170,7 @@ func (z *ComplexNumber) QuoRem(x, y, r *ComplexNumber) (*ComplexNumber, *Complex // If Euclidean inequality already holds we're done. // Otherwise walk ≤2 unit steps in the hex lattice until N(r) < N(y). - for r.Norm().Cmp(norm) >= 0 { + if r.Norm().Cmp(norm) >= 0 { bestQ0, bestQ1 := new(big.Int).Set(z.A0), new(big.Int).Set(z.A1) bestR := new(ComplexNumber).Set(r) bestN2 := bestR.Norm()