1.6. Phương pháp Coppersmith¶
Phương pháp Coppersmith được dùng để tìm nghiệm nhỏ của đa thức trên modulo. Phần này mình tham khảo chính từ [34].
1.6.1. Ý tưởng¶
Giả sử ta có phương trình \(F(x) \equiv 0 \bmod M\). Với số \(X\) cố định cho trước, phương pháp Coppersmith cho phép tìm nghiệm \(x_0\) nhỏ thỏa mãn \(\lvert x_0 \rvert \leqslant X\).
Ý tưởng của phương pháp này là thay vì tìm nghiệm \(x_0\) của \(F(x)\) trên modulo \(M\), chúng ta sẽ mở rộng lên, tìm một hàm \(G(x)\) nào đó mà có nghiệm \(x_0\) trên \(\mathbb{Z}\).
Đơn giản nhất là
và \(\deg g(x) \leqslant \deg F(x)\). Rõ ràng khi modulo hai vế cho \(M\) thì \(G(x_0) = F(x_0) = 0 \bmod M\).
Phương pháp này giúp tìm nghiệm của một đa thức bậc nhỏ modulo \(M\). Do đó giả sử đặt:
với \(a_i \in \mathbb{Z}\).
Lúc này chúng ta sẽ tìm hàm \(G(x)\) trên với hệ số nhỏ.
Giả sử
với \(b_i \in \mathbb{Z}\).
Khi đó
Ta mong muốn các hệ số \(k \cdot a_0 + M \cdot b_0\), \(k \cdot a_1 + M \cdot b_1\), \(\cdots\), \(k \cdot a_d + M \cdot b_d\) nhỏ so với \(M\).
Do đó với số \(X\) cho trước, nếu ta xây lattice (\(d+2\) vector) sau:
Khi đó hệ số của mỗi dòng từ \(\bm{v}_0\) tới \(\bm{v}_d\) là \(b_0\) tới \(b_d\). Còn hệ số của \(\bm{v}_{d+1}\) là \(k\).
Tuy nhiên chúng ta thường sẽ biến đổi để đa thức trở thành monic (\(a_d = 1\)). Khi đó chúng ta bỏ đi \(v_d\) và còn \(d+1\) vector trong lattice.
1.6.2. Cải tiến thuật toán¶
1.6.2.1. Dạng đơn giản¶
Giả sử \(k(x)\) có bậc là \(h\). Đặt \(k(x) = c_0 + c_1 x + \cdots + c_h x^h\).
Khi đó:
Lúc này mỗi đại lượng \(F(x)\), \(xF(x)\), ..., \(x^h F(x)\) sẽ khiến hệ số của \(F(x)\) ban đầu tăng bậc. Nói cách khác hệ số \(a_i\) của \(x^i\) trong \(F(x)\) sẽ là hệ số của \(x^{i+j}\) trong \(x^j F(x)\).
Sách giáo khoa nói rằng nếu mình chọn \(h = d - 1\) thì phương pháp Coppersmith sẽ cho ra kết quả nếu \(X\) được chọn thỏa \(X \approx M^{1/(2d-1)}\).
Tương tự, mình sẽ có các vector trong lattice như sau:
Sau đó thêm các vector khi shift vào, tương ứng với \(xF(x)\), \(x^2 F(x)\), ...
Tuy nhiên vấn đề ở đây là bound của nghiệm bị thu hẹp lại. Ban nãy mình nói rằng nếu chọn \(h=d-1\) thì nghiệm cho kết quả nếu \(X \approx M^{1/(2d-1)}\). Ở đây \(M = 10001\) nên \(X \approx 4\). Trong khi ở bài viết trước thì \(X = 10\). Do đó có thể thấy việc mở rộng này đôi khi kém hiệu quả tùy thuộc vào \(h\).
1.6.2.2. Dạng nâng cao¶
Coppersmith đã đề xuất ý tưởng như sau:
Bổ đề 1.1 (Coppersmith)
Với \(0 < \epsilon < \min \{0,18; 1/d\}\). Đặt \(F(x)\) là đa thức monic bậc \(d\) có một hoặc nhiều nghiệm \(x_0\) trên modulo \(M\) sao cho \(\lvert x_0 \rvert \leqslant \frac{1}{2} M^{1/d-\epsilon}\). Khi đó \(x_0\) có thể tính với thời gian đa thức giới hạn bởi \(d\), \(1/\epsilon\) và \(\log(M)\).
Để chứng minh bổ đề này thì Coppersmith đã xây dựng một hệ lattice để tính toán và cũng là cách xây dựng lattice sẽ đề cập tới đây.
Chúng ta biết rằng \(x_0\) là nghiệm của đa thức \(F(x) \equiv 0 \bmod M\). Do đó ta suy ra \(F(x_0)^k \equiv 0 \bmod M^k\).
Từ nhận xét này, mình mở rộng phần cơ bản lên tìm nghiệm trong modulo \(M^{h-1}\) với \(h\) là một số được chọn trước.
Với \(k(x) = c_0 + c_1 x + \cdots + c_{d-1} x^{d-1}\) biểu diễn việc shift thành \(F(x)\), \(xF(x)\), \(x^2 F(x)\), ..., \(x^{d-1} F(x)\).
Ta xét \(h\) đa thức sau:
Với mỗi đa thức \(M^{h-1-j} F(x)^j\) chúng ta có \(d\) lần shift tương ứng với từng hệ số của \(k(x)\). Cụ thể là \(M^{h-1-j}F(x)^j\), \(M^{h-1-j}F(x)^j x\), \(M^{h-1-j}F(x)^j x^2\), ..., \(M^{h-1-j}F(x)^j x^{d-1}\).
Do đó có tất cả \(dh\) vector trong lattice. Số \(h\) thường được chọn sao cho \(dh \approx \epsilon\). Và chặn nghiệm \(X\) có thể chọn là \(\frac{1}{2}M^{1/d - \epsilon}\) theo như bổ đề.
Như vậy mình xây lattice như sau:
Bước 1. Với \(M^{h-1} F(x)^0\) thì các vector sau lần lượt tương ứng với
Nói cách khác thì:
Bước 2. Với \(M^{h-2} F(x)\) thì các vector sau lần lượt tương ứng với
Nói cách khác thì:
Bước thứ \(j\). Cứ như vậy với \(M^{h-1-j}F(x)^j\).
Chạy LLL trên lattice trên sẽ cho kết quả ^^.
1.6.3. Code thử nghiệm¶
1.6.3.1. So sánh ví dụ đơn giản với small_roots của SageMath¶
from sage.all import *
def small_roots(f, M, X):
d = f.degree()
P = f.base_ring()
B = []
for i in range(d):
b = [0] * (d + 1)
b[i] = M * X**i
B.append(b)
B.append([j*X**i for i,j in enumerate(f.coefficients())])
B = Matrix(ZZ, B)
B = B.LLL()
bf = B[0]
g = sum((j//(X**i)) * x**i for i,j in enumerate(bf))
roots = g.roots()
return [root[0] for root in roots]
M = 10001
X = 10
P = PolynomialRing(ZZ, 'x')
Q = PolynomialRing(Zmod(M), 'xn')
x = P.gen()
xn = Q.gen()
f = x**3 + 10*x**2 + 5000*x - 222
g = f.change_ring(Q).subs(x=xn)
print(small_roots(f, M, X))
print(g.small_roots(X=10))
1.6.3.2. Cải tiến dạng đơn giản¶
from sage.all import *
def small_roots(f, M, X):
d = f.degree()
B = []
for i in range(d):
b = [0] * (2*d)
b[i] = M * X**i
B.append(b)
for i in range(d):
g = x**i * f
b = [v * X**u for u, v in enumerate(g.coefficients(sparse=False))]
b = b + [0] * (2*d - len(b))
B.append(b)
B = Matrix(ZZ, B)
B = B.LLL()
bf = B[0]
g = sum((j // (X**i)) * x**i for i, j in enumerate(bf))
roots = g.roots()
return [root[0] for root in roots]
M = 10001
X = 10
P = PolynomialRing(ZZ, 'x')
x = P.gen()
f = x**3 + 10*x**2 + 5000*x - 222
print(small_roots(f, M, X))
1.6.3.3. Cải tiến dạng nâng cao¶
from sage.all import *
def small_roots(f, M, h = None, epsilon = None, X = None):
d = f.degree()
if not h:
h = d
if not epsilon:
epsilon = 1/(d*h)
if not X:
X = round(0.5*M**(1/d-epsilon))
B = []
for j in range(h):
g = M**(h-1-j) * f**j
for i in range(d):
k = g * x**i
b = [v * X**u for u, v in enumerate(k.coefficients(sparse=False))]
b = b + [0] * (d*h - len(b))
B.append(b)
B = Matrix(ZZ, B)
B = B.LLL()
bf = B[0]
g = sum((j // (X**i)) * x**i for i, j in enumerate(bf))
roots = g.roots()
return [root[0] for root in roots]
# Test theo sách giáo khoa
M = (2**30 + 3)*(2**32 + 15)
P = PolynomialRing(ZZ, 'x')
x = P.gen()
f = 1942528644709637042 + 1234567890123456789*x + 987654321987654321*x**2 + x**3
print(small_roots(f, M))
# Tự test
M = (2**20 + 7)*(2**21 + 17)
P = PolynomialRing(ZZ, 'x')
x = P.gen()
f = x**3 + (2**25 - 2883584)*x**2 + 46976195*x + 227
print(small_roots(f, M, X = 2**9))