From 9b4b2e799afdb28ad1309784e494855685e9f8cf Mon Sep 17 00:00:00 2001 From: Gareth Ma Date: Thu, 2 Jan 2025 09:15:14 +0800 Subject: [PATCH 1/7] add pari option to polynomial `small_roots` --- .../polynomial/polynomial_modn_dense_ntl.pyx | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx index 68605382f1f..0aec7fb9478 100644 --- a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx +++ b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx @@ -468,7 +468,7 @@ cdef class Polynomial_dense_mod_n(Polynomial): modular_composition = compose_mod -def small_roots(self, X=None, beta=1.0, epsilon=None, **kwds): +def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="sage", **kwds): r""" Let `N` be the characteristic of the base ring this polynomial is defined over: ``N = self.base_ring().characteristic()``. @@ -490,6 +490,7 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, **kwds): - ``beta`` -- compute a root mod `b` where `b` is a factor of `N` and `b \ge N^\beta` (default: 1.0, so `b = N`.) - ``epsilon`` -- the parameter `\epsilon` described above. (default: `\beta/8`) + - ``algorithm`` -- ``"sage"`` (default) or ``"pari"`` - ``**kwds`` -- passed through to method :meth:`Matrix_integer_dense.LLL() ` EXAMPLES: @@ -646,27 +647,36 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, **kwds): X = (0.5 * N**(beta**2/delta - epsilon)).ceil() verbose("X = %s"%X, level=2) - # we could do this much faster, but this is a cheap step - # compared to LLL - g = [x**j * N**(m-i) * f**i for i in range(m) for j in range(delta) ] - g.extend([x**i * f**m for i in range(t)]) # h + Nbeta = N**beta + ZmodN = self.base_ring() - B = Matrix(ZZ, len(g), delta*m + max(delta,t) ) - for i in range(B.nrows()): - for j in range( g[i].degree()+1 ): - B[i,j] = g[i][j]*X**j + if algorithm == "pari": + roots = set(map(ZmodN, pari.zncoppersmith(f, N, X=X, B=Nbeta.floor()))) - B = B.LLL(**kwds) + elif algorithm == "sage": + # we could do this much faster, but this is a cheap step + # compared to LLL + g = [x**j * N**(m-i) * f**i for i in range(m) for j in range(delta) ] + g.extend([x**i * f**m for i in range(t)]) # h - f = sum([ZZ(B[0,i]//X**i)*x**i for i in range(B.ncols())]) - R = f.roots() + B = Matrix(ZZ, len(g), delta*m + max(delta,t) ) + for i in range(B.nrows()): + for j in range( g[i].degree()+1 ): + B[i,j] = g[i][j]*X**j + + B = B.LLL(**kwds) + + f = sum([ZZ(B[0,i]//X**i)*x**i for i in range(B.ncols())]) + R = f.roots() + roots = set([ZmodN(r) for r,m in R if abs(r) <= X]) + + else: + raise ValueError('algorithm must be "pari" or "sage"') - ZmodN = self.base_ring() - roots = set([ZmodN(r) for r,m in R if abs(r) <= X]) - Nbeta = N**beta return [root for root in roots if N.gcd(ZZ(self(root))) >= Nbeta] + cdef class Polynomial_dense_modn_ntl_zz(Polynomial_dense_mod_n): r""" Polynomial on `\ZZ/n\ZZ` implemented via NTL. From 849727eb07c2f711fb62f4ea3a993f9599b7c9b3 Mon Sep 17 00:00:00 2001 From: Gareth Ma Date: Thu, 2 Jan 2025 09:42:13 +0800 Subject: [PATCH 2/7] add example to small_roots --- .../polynomial/polynomial_modn_dense_ntl.pyx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx index 0aec7fb9478..e238e1c5523 100644 --- a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx +++ b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx @@ -580,12 +580,9 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="sage", **kwds): sage: q = next_prime(round(pi.n()*p)) # needs sage.symbolic sage: N = p*q # needs sage.symbolic - Now we disturb the low 110 bits of `q`:: + Now we disturb the low 110 bits of `q` and try to recover `q` from it:: sage: qbar = q + ZZ.random_element(0, 2^hidden - 1) # needs sage.symbolic - - And try to recover `q` from it:: - sage: F. = PolynomialRing(Zmod(N), implementation='NTL') # needs sage.symbolic sage: f = x - qbar # needs sage.symbolic @@ -603,6 +600,27 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="sage", **kwds): sage: q == qbar - d # needs sage.symbolic True + In general, using ``algorithm="pari"`` will return better results and be quicker. For instance, + it is able to recover `q` even if ``hidden`` is set to `120`:: + + sage: # needs sage.symbolic + sage: set_random_seed(1337) + sage: hidden = 120 + sage: N = p*q + sage: qbar = q + ZZ.random_element(0, 2^hidden - 1) + sage: f = x - qbar + sage: set_verbose(0) + sage: f.small_roots(X=2^hidden-1, beta=0.5, algorithm="sage") # time random + [] + sage: f.small_roots(X=2^hidden-1, beta=0.5, algorithm="pari") # time random + [1203913112977791332288506012179577388] + sage: qbar - q == _[0] + True + + .. TODO:: + + Implement improved lattice constructions for small_roots. + REFERENCES: Don Coppersmith. *Finding a small root of a univariate modular equation.* From 44292900371b1d99a99f5b3e600c0b49a5c6fe59 Mon Sep 17 00:00:00 2001 From: Gareth Ma Date: Thu, 2 Jan 2025 09:58:31 +0800 Subject: [PATCH 3/7] make pari the default --- src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx index e238e1c5523..11b872b7530 100644 --- a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx +++ b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx @@ -468,7 +468,7 @@ cdef class Polynomial_dense_mod_n(Polynomial): modular_composition = compose_mod -def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="sage", **kwds): +def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="pari", **kwds): r""" Let `N` be the characteristic of the base ring this polynomial is defined over: ``N = self.base_ring().characteristic()``. @@ -490,7 +490,7 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="sage", **kwds): - ``beta`` -- compute a root mod `b` where `b` is a factor of `N` and `b \ge N^\beta` (default: 1.0, so `b = N`.) - ``epsilon`` -- the parameter `\epsilon` described above. (default: `\beta/8`) - - ``algorithm`` -- ``"sage"`` (default) or ``"pari"`` + - ``algorithm`` -- ``"sage"`` or ``"pari"`` (default) - ``**kwds`` -- passed through to method :meth:`Matrix_integer_dense.LLL() ` EXAMPLES: From 69723d2b6d5dcd1a766cbf9c99b549f9dbee7668 Mon Sep 17 00:00:00 2001 From: Gareth Ma Date: Thu, 2 Jan 2025 12:09:40 +0800 Subject: [PATCH 4/7] fix lint --- src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx index 11b872b7530..0b202478ae2 100644 --- a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx +++ b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx @@ -694,7 +694,6 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="pari", **kwds): return [root for root in roots if N.gcd(ZZ(self(root))) >= Nbeta] - cdef class Polynomial_dense_modn_ntl_zz(Polynomial_dense_mod_n): r""" Polynomial on `\ZZ/n\ZZ` implemented via NTL. From 831b70f344c67455b59d20468a9ab1f5a280dee4 Mon Sep 17 00:00:00 2001 From: Gareth Ma Date: Thu, 2 Jan 2025 12:31:56 +0800 Subject: [PATCH 5/7] fix seed to avoid pari stack overflow --- src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx index 0b202478ae2..9722bd762b8 100644 --- a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx +++ b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx @@ -574,6 +574,7 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="pari", **kwds): First, we set up `p`, `q` and `N`:: + sage: set_random_seed(1337) sage: length = 512 sage: hidden = 110 sage: p = next_prime(2^int(round(length/2))) @@ -604,9 +605,7 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="pari", **kwds): it is able to recover `q` even if ``hidden`` is set to `120`:: sage: # needs sage.symbolic - sage: set_random_seed(1337) sage: hidden = 120 - sage: N = p*q sage: qbar = q + ZZ.random_element(0, 2^hidden - 1) sage: f = x - qbar sage: set_verbose(0) From 1b57621c86d97e89a5aa88c142a62d44df50bbf1 Mon Sep 17 00:00:00 2001 From: Gareth Ma Date: Thu, 2 Jan 2025 12:44:27 +0800 Subject: [PATCH 6/7] fix docstrings --- src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx index 9722bd762b8..d0be4ef4c90 100644 --- a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx +++ b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx @@ -592,12 +592,11 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="pari", **kwds): sage: from sage.misc.verbose import set_verbose sage: set_verbose(2) - sage: d = f.small_roots(X=2^hidden-1, beta=0.5)[0] # time random # needs sage.symbolic + sage: d = f.small_roots(X=2^hidden-1, beta=0.5)[0] # needs sage.symbolic + verbose 2 () epsilon = 0.062500 verbose 2 () m = 4 verbose 2 () t = 4 verbose 2 () X = 1298074214633706907132624082305023 - verbose 1 () LLL of 8x8 matrix (algorithm fpLLL:wrapper) - verbose 1 () LLL finished (time = 0.006998) sage: q == qbar - d # needs sage.symbolic True From 137dad7f3618b6302ed00c8b0f3069cf51ade543 Mon Sep 17 00:00:00 2001 From: Gareth Ma Date: Thu, 9 Jan 2025 12:45:26 +0900 Subject: [PATCH 7/7] rename algorithm to small_roots_algorithm --- .../polynomial/polynomial_modn_dense_ntl.pyx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx index d0be4ef4c90..66e0af9a909 100644 --- a/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx +++ b/src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx @@ -468,7 +468,7 @@ cdef class Polynomial_dense_mod_n(Polynomial): modular_composition = compose_mod -def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="pari", **kwds): +def small_roots(self, X=None, beta=1.0, epsilon=None, small_roots_algorithm="pari", **kwds): r""" Let `N` be the characteristic of the base ring this polynomial is defined over: ``N = self.base_ring().characteristic()``. @@ -486,11 +486,11 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="pari", **kwds): INPUT: - - ``X`` -- an absolute bound for the root (default: see above) + - ``X`` -- an absolute bound for the root (default: see above.) - ``beta`` -- compute a root mod `b` where `b` is a factor of `N` and `b \ge N^\beta` (default: 1.0, so `b = N`.) - ``epsilon`` -- the parameter `\epsilon` described above. (default: `\beta/8`) - - ``algorithm`` -- ``"sage"`` or ``"pari"`` (default) + - ``small_roots_algorithm`` -- ``"sage"`` or ``"pari"`` (default.) - ``**kwds`` -- passed through to method :meth:`Matrix_integer_dense.LLL() ` EXAMPLES: @@ -600,17 +600,17 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="pari", **kwds): sage: q == qbar - d # needs sage.symbolic True - In general, using ``algorithm="pari"`` will return better results and be quicker. For instance, - it is able to recover `q` even if ``hidden`` is set to `120`:: + In general, using ``small_roots_algorithm="pari"`` will return better results and be quicker. + For instance, it is able to recover `q` even if ``hidden`` is set to `120`:: sage: # needs sage.symbolic sage: hidden = 120 sage: qbar = q + ZZ.random_element(0, 2^hidden - 1) sage: f = x - qbar sage: set_verbose(0) - sage: f.small_roots(X=2^hidden-1, beta=0.5, algorithm="sage") # time random + sage: f.small_roots(X=2^hidden-1, beta=0.5, small_roots_algorithm="sage") # time random [] - sage: f.small_roots(X=2^hidden-1, beta=0.5, algorithm="pari") # time random + sage: f.small_roots(X=2^hidden-1, beta=0.5, small_roots_algorithm="pari") # time random [1203913112977791332288506012179577388] sage: qbar - q == _[0] True @@ -666,10 +666,16 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, algorithm="pari", **kwds): Nbeta = N**beta ZmodN = self.base_ring() - if algorithm == "pari": - roots = set(map(ZmodN, pari.zncoppersmith(f, N, X=X, B=Nbeta.floor()))) + if small_roots_algorithm == "pari": + from sage.libs.pari.all import PariError + try: + roots = set(map(ZmodN, pari.zncoppersmith(f, N, X=X, B=Nbeta.floor()))) + except PariError: + # according to my testing, Sage is never able to return a root when Pari fails (usually + # due to OOM.) See discussion in #39243. + roots = set([]) - elif algorithm == "sage": + elif small_roots_algorithm == "sage": # we could do this much faster, but this is a cheap step # compared to LLL g = [x**j * N**(m-i) * f**i for i in range(m) for j in range(delta) ]