Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to use pari for small_roots #39243

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 55 additions & 23 deletions src/sage/rings/polynomial/polynomial_modn_dense_ntl.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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()``.
Expand All @@ -486,10 +486,11 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, **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`)
- ``small_roots_algorithm`` -- ``"sage"`` or ``"pari"`` (default.)
- ``**kwds`` -- passed through to method :meth:`Matrix_integer_dense.LLL() <sage.matrix.matrix_integer_dense.Matrix_integer_dense.LLL>`

EXAMPLES:
Expand Down Expand Up @@ -573,18 +574,16 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, **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)))
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.<x> = PolynomialRing(Zmod(N), implementation='NTL') # needs sage.symbolic
sage: f = x - qbar # needs sage.symbolic

Expand All @@ -593,15 +592,33 @@ def small_roots(self, X=None, beta=1.0, epsilon=None, **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 (<module>) epsilon = 0.062500
verbose 2 (<module>) m = 4
verbose 2 (<module>) t = 4
verbose 2 (<module>) X = 1298074214633706907132624082305023
verbose 1 (<module>) LLL of 8x8 matrix (algorithm fpLLL:wrapper)
verbose 1 (<module>) LLL finished (time = 0.006998)
sage: q == qbar - d # needs sage.symbolic
True

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, small_roots_algorithm="sage") # 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

.. TODO::

Implement improved lattice constructions for small_roots.

REFERENCES:

Don Coppersmith. *Finding a small root of a univariate modular equation.*
Expand Down Expand Up @@ -646,24 +663,39 @@ 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 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([])

B = B.LLL(**kwds)
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) ]
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]


Expand Down
Loading