Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
jack4818
GitHub Repository: jack4818/Castryck-Decru-SageMath
Path: blob/main/richelot_aux.py
323 views
1
# SageMath imports
2
from sage.all import (
3
PolynomialRing,
4
EllipticCurve,
5
EllipticCurveIsogeny,
6
HyperellipticCurve,
7
Matrix,
8
vector,
9
ZZ,
10
set_verbose
11
)
12
13
set_verbose(-1)
14
15
def FromProdToJac(C, E, P_c, Q_c, P, Q, a):
16
Fp2 = C.base()
17
Rx = PolynomialRing(Fp2, name="x")
18
x = Rx.gens()[0]
19
20
P_c2 = 2**(a-1)*P_c
21
Q_c2 = 2**(a-1)*Q_c
22
P2 = 2**(a-1)*P
23
Q2 = 2**(a-1)*Q
24
25
a1, a2, a3 = P_c2[0], Q_c2[0], (P_c2 + Q_c2)[0]
26
b1, b2, b3 = P2[0], Q2[0], (P2 + Q2)[0]
27
28
# Compute coefficients
29
M = Matrix(Fp2, [
30
[a1*b1, a1, b1],
31
[a2*b2, a2, b2],
32
[a3*b3, a3, b3]])
33
R, S, T = M.inverse() * vector(Fp2, [1,1,1])
34
RD = R * M.determinant()
35
da = (a1 - a2)*(a2 - a3)*(a3 - a1)
36
db = (b1 - b2)*(b2 - b3)*(b3 - b1)
37
38
s1, t1 = - da / RD, db / RD
39
s2, t2 = -T/R, -S/R
40
41
a1_t = (a1 - s2) / s1
42
a2_t = (a2 - s2) / s1
43
a3_t = (a3 - s2) / s1
44
h = s1 * (x**2 - a1_t) * (x**2 - a2_t) * (x**2 - a3_t)
45
46
H = HyperellipticCurve(h)
47
J = H.jacobian()
48
49
def isogeny(pair):
50
# Argument members may be None to indicate the zero point.
51
52
# The projection maps are:
53
# H->C: (xC = s1/x²+s2, yC = s1 y)
54
# so we compute Mumford coordinates of the divisor f^-1(P_c): a(x), y-b(x)
55
Pc, P = pair
56
if Pc:
57
xPc, yPc = Pc.xy()
58
JPc = J([s1 * x**2 + s2 - xPc, Rx(yPc / s1)])
59
# Same for E
60
# H->E: (xE = t1 x² + t2, yE = t1 y/x^3)
61
if P:
62
xP, yP = P.xy()
63
JP = J([(xP - t2) * x**2 - t1, yP * x**3 / t1])
64
if Pc and P:
65
return JPc + JP
66
if Pc:
67
return JPc
68
if P:
69
return JP
70
71
imPcP = isogeny((P_c, P))
72
imQcQ = isogeny((Q_c, Q))
73
74
# Validate result, for debugging
75
# def projC(_x, _y):
76
# return (s1 * _x^2 + s2, s1 * _y)
77
# def projE(_x, _y):
78
# return (t1 / _x^2 + t2, t1 * _y / _x^3)
79
# Fp4 = Fp2.extension(2)
80
# E4 = E.change_ring(Fp4)
81
# C4 = C.change_ring(Fp4)
82
# divP = [(xr, imPcP[1](xr)) for xr, _ in imPcP[0].roots(Fp4)]
83
# assert 2*E4(P) == sum(E4(*projE(*pt)) for pt in divP)
84
# assert 2*C4(P_c) == sum(C4(*projC(*pt)) for pt in divP)
85
# divQ = [(xr, imQcQ[1](xr)) for xr, _ in imQcQ[0].roots(Fp4)]
86
# assert 2*E4(Q) == sum(E4(*projE(*pt)) for pt in divQ)
87
# assert 2*C4(Q_c) == sum(C4(*projC(*pt)) for pt in divQ)
88
89
return h, imPcP[0], imPcP[1], imQcQ[0], imQcQ[1], isogeny
90
91
class RichelotCorr:
92
"""
93
The Richelot correspondance between hyperelliptic
94
curves y²=g1*g2*g3 and y²=h1*h2*h3=hnew(x)
95
96
It is defined by equations:
97
g1(x1) h1(x2) + g2(x1) h2(x2) = 0
98
and y1 y2 = g1(x1) h1(x2) (x1 - x2)
99
100
Given a divisor D in Mumford coordinates:
101
U(x) = x^2 + u1 x + u0 = 0
102
y = V(x) = v1 x + v0
103
Let xa and xb be the symbolic roots of U.
104
Let s, p by the sum and product (s=-u1, p=u0)
105
106
Then on x-coordinates, the image of D is defined by equation:
107
(g1(xa) h1(x) + g2(xa) h2(x))
108
* (g1(xb) h1(x) + g2(xb) h2(x))
109
which is a symmetric function of xa and xb.
110
This is a non-reduced polynomial of degree 4.
111
112
Write gred = g-U = g1*x + g0
113
then gred(xa) gred(xb) = g1^2*p + g1*g0*s + g0^2
114
and g1red(xa) g2red(xb) + g1red(xb) g2red(xa)
115
= 2 g11 g21 p + (g11*g20+g10*g21) s + 2 g10*g20
116
117
On y-coordinates, the image of D is defined by equations:
118
V(xa) y = Gred1(xa) h1(x) (xa - x)
119
OR V(xb) y = Gred1(xb) h1(x) (xb - x)
120
If we multiply:
121
* y^2 has coefficient V(xa)V(xb)
122
* y has coefficient h1(x) * (V(xa) Gred1(xb) (x-xb) + V(xb) Gred1(xa) (x-xa))
123
(x-degree 3)
124
* 1 has coefficient Gred1(xa) Gred1(xb) h1(x)^2 (x-xa)(x-xb)
125
= Gred1(xa) Gred1(xb) h1(x)^2 U(x)
126
(x-degree 4)
127
"""
128
def __init__(self, G1, G2, H1, H2, hnew):
129
assert G1[2].is_one() and G2[2].is_one()
130
self.G1 = G1
131
self.G2 = G2
132
self.H1 = H1
133
self.H11 = H1*H1
134
self.H12 = H1*H2
135
self.H22 = H2*H2
136
self.hnew = hnew
137
self.x = hnew.parent().gen()
138
139
def map(self, D):
140
"Computes (non-monic) Mumford coordinates for the image of D"
141
U, V = D
142
if not U[2].is_one():
143
U = U / U[2]
144
V = V % U
145
# Sum and product of (xa, xb)
146
s, p = -U[1], U[0]
147
# Compute X coordinates (non reduced, degree 4)
148
g1red = self.G1 - U
149
g2red = self.G2 - U
150
assert g1red[2].is_zero() and g2red[2].is_zero()
151
g11, g10 = g1red[1], g1red[0]
152
g21, g20 = g2red[1], g2red[0]
153
# see above
154
Px = (g11*g11*p + g11*g10*s + g10*g10) * self.H11 \
155
+ (2*g11*g21*p + (g11*g20+g21*g10)*s + 2*g10*g20) * self.H12 \
156
+ (g21*g21*p + g21*g20*s + g20*g20) * self.H22
157
158
# Compute Y coordinates (non reduced, degree 3)
159
assert V[2].is_zero()
160
v1, v0 = V[1], V[0]
161
# coefficient of y^2 is V(xa)V(xb)
162
Py2 = v1*v1*p + v1*v0*s + v0*v0
163
# coefficient of y is h1(x) * (V(xa) Gred1(xb) (x-xb) + V(xb) Gred1(xa) (x-xa))
164
# so we need to symmetrize:
165
# V(xa) Gred1(xb) (x-xb)
166
# = (v1*xa+v0)*(g11*xb+g10)*(x-xb)
167
# = (v1*g11*p + v1*g10*xa + v0*g11*xb + v0*g10)*x
168
# - xb*(v1*g11*p + v1*g10*xa + v0*g11*xb + v0*g10)
169
# Symmetrizing xb^2 gives u1^2-2*u0
170
Py1 = (2*v1*g11*p + v1*g10*s + v0*g11*s + 2*v0*g10)*self.x \
171
- (v1*g11*s*p + 2*v1*g10*p + v0*g11*(s*s-2*p) + v0*g10*s)
172
Py1 *= self.H1
173
# coefficient of 1 is Gred1(xa) Gred1(xb) h1(x)^2 U(x)
174
Py0 = self.H11 * U * (g11*g11*p + g11*g10*s + g10*g10)
175
176
# Now reduce the divisor, and compute Cantor reduction.
177
# Py2 * y^2 + Py1 * y + Py0 = 0
178
# y = - (Py2 * hnew + Py0) / Py1
179
_, Py1inv, _ = Py1.xgcd(Px)
180
Py = (- Py1inv * (Py2 * self.hnew + Py0)) % Px
181
assert Px.degree() == 4
182
assert Py.degree() <= 3
183
184
Dx = ((self.hnew - Py ** 2) // Px)
185
Dy = (-Py) % Dx
186
return (Dx, Dy)
187
188
def jacobian_double(h, u, v):
189
"""
190
Computes the double of a jacobian point (u,v)
191
given by Mumford coordinates: except that u is not required
192
to be monic, to avoid redundant reduction during repeated doubling.
193
194
See SAGE cantor_composition() and cantor_reduction
195
"""
196
assert u.degree() == 2
197
# Replace u by u^2
198
# Compute h3 the inverse of 2*v modulo u
199
# Replace v by (v + h3 * (h - v^2)) % u
200
q, r = u.quo_rem(2*v)
201
if r[0] == 0: # gcd(u, v) = v, very improbable
202
a = q**2
203
b = (v + (h - v**2) // v) % a
204
return a, b
205
else: # gcd(u, v) = 1
206
h3 = 1 / (-r[0]) * q
207
a = u*u
208
b = (v + h3 * (h - v**2)) % a
209
# Cantor reduction
210
Dx = (h - b**2) // a
211
Dy = (-b) % Dx
212
return Dx, Dy
213
214
def jacobian_iter_double(h, u, v, n):
215
for _ in range(n):
216
u, v = jacobian_double(h, u, v)
217
return u.monic(), v
218
219
def FromJacToJac(h, D11, D12, D21, D22, a, powers=None):
220
# power is an optional list of precomputed tuples
221
# (l, 2^l D1, 2^l D2) where l < a are increasing
222
R,x = h.parent().objgen()
223
Fp2 = R.base()
224
225
D1 = (D11, D12)
226
D2 = (D21, D22)
227
228
next_powers = None
229
if not powers:
230
# Precompute some powers of D1, D2 to save computations later.
231
# We are going to perform O(a^1.5) squarings instead of O(a^2)
232
if a >= 16:
233
gap = ZZ(a).isqrt()
234
doubles = [(0, D1, D2)]
235
_D1, _D2 = D1, D2
236
for i in range(a-1):
237
_D1 = jacobian_double(h, _D1[0], _D1[1])
238
_D2 = jacobian_double(h, _D2[0], _D2[1])
239
doubles.append((i+1, _D1, _D2))
240
_, (G1, _), (G2, _) = doubles[a-1]
241
G1, G2 = G1.monic(), G2.monic()
242
next_powers = [doubles[a-2*gap], doubles[a-gap]]
243
else:
244
G1, _ = jacobian_iter_double(h, D1[0], D1[1], a-1)
245
G2, _ = jacobian_iter_double(h, D2[0], D2[1], a-1)
246
else:
247
(l, _D1, _D2) = powers[-1]
248
if a >= 16:
249
next_powers = powers if l < a-1 else powers[:-1]
250
G1, _ = jacobian_iter_double(h, _D1[0], _D1[1], a-1-l)
251
G2, _ = jacobian_iter_double(h, _D2[0], _D2[1], a-1-l)
252
253
# assert 2**a*D1 == 0
254
# assert 2**a*D2 == 0
255
G3, r3 = h.quo_rem(G1 * G2)
256
assert r3 == 0
257
258
delta = Matrix(G.padded_list(3) for G in (G1,G2,G3))
259
# H1 = 1/det (G2[1]*G3[0] - G2[0]*G3[1])
260
# +2x (G2[2]*G3[0] - G3[2]*G2[0])
261
# +x^2(G2[1]*G3[2] - G3[1]*G2[2])
262
# The coefficients correspond to the inverse matrix of delta.
263
delta = delta.inverse()
264
H1 = -delta[0][0]*x**2 + 2*delta[1][0]*x - delta[2][0]
265
H2 = -delta[0][1]*x**2 + 2*delta[1][1]*x - delta[2][1]
266
H3 = -delta[0][2]*x**2 + 2*delta[1][2]*x - delta[2][2]
267
268
hnew = H1*H2*H3
269
270
# Now compute image points: Richelot isogeny is defined by the degree 2
271
R = RichelotCorr(G1, G2, H1, H2, hnew)
272
273
imD1 = R.map(D1)
274
imD2 = R.map(D2)
275
if next_powers:
276
next_powers = [(l, R.map(_D1), R.map(_D2))
277
for l, _D1, _D2 in next_powers]
278
return hnew, imD1[0], imD1[1], imD2[0], imD2[1], R.map, next_powers
279
280
def FromJacToProd(G1, G2, G3):
281
"""
282
Construct the "split" isogeny from Jac(y^2 = G1*G2*G3)
283
to a product of elliptic curves.
284
285
This computation is the same as Benjamin Smith
286
see 8.3 in http://iml.univ-mrs.fr/~kohel/phd/thesis_smith.pdf
287
"""
288
h = G1*G2*G3
289
R = h.parent()
290
Fp2 = R.base()
291
x = R.gen()
292
293
M = Matrix(G.padded_list(3) for G in (G1,G2,G3))
294
# Find homography
295
u, v, w = M.right_kernel().gen()
296
d = u/2
297
(ad, _), (b, _) = (x**2 - v*x + w*d/2).roots()
298
a = ad/d
299
300
# Apply transform G(x) -> G((a*x+b)/(x+d))*(x+d)^2
301
# The coefficients of x^2 are M * (1, a, a^2)
302
# The coefficients of 1 are M * (d^2, b*d, b^2)
303
H11, H21, H31 = M * vector([1, a, a*a])
304
H10, H20, H30 = M * vector([d*d, b*d, b*b])
305
assert G1((a*x+b)/(x+d))*(x+d)**2 == H11*x**2+H10
306
307
h2 = (H11*x**2+H10)*(H21*x**2+H20)*(H31*x**2+H30)
308
H2 = HyperellipticCurve(h2)
309
310
p1 = (H11*x+H10)*(H21*x+H20)*(H31*x+H30)
311
p2 = (H11+H10*x)*(H21+H20*x)*(H31+H30*x)
312
# We will need to map to actual elliptic curve
313
p1norm = (x + H10*H21*H31)*(x + H20*H11*H31)*(x + H30*H11*H21)
314
p2norm = (x + H11*H20*H30)*(x + H21*H10*H30)*(x + H31*H10*H20)
315
E1 = EllipticCurve([0, p1norm[2], 0, p1norm[1], p1norm[0]])
316
E2 = EllipticCurve([0, p2norm[2], 0, p2norm[1], p2norm[0]])
317
318
def morphE1(x, y):
319
# from y^2=p1 to y^2=p1norm
320
return (H11*H21*H31*x, H11*H21*H31*y)
321
def morphE2(x, y):
322
# from y^2=p1 to y^2=p2norm
323
return (H10*H20*H30*x, H10*H20*H30*y)
324
# The morphisms are:
325
# inverse homography:
326
# H->H2: x, y => ((b-dx) / (x-a), y/(x-a)^3)
327
# then H2->E1:(x,y) => (x^2,y)
328
# or H2->E2:(x,y) => (1/x^2,y/x^3)
329
330
def isogeny(D):
331
HyperellipticCurve(h).jacobian()(D)
332
# To map a divisor, perform the change of coordinates
333
# on Mumford coordinates
334
U, V = D
335
# apply homography
336
# y = v1 x + v0 =>
337
U_ = U[0] * (x+d)**2 + U[1]*(a*x+b)*(x+d) + U[2]*(a*x+b)**2
338
V_ = V[0] * (x+d)**3 + V[1]*(a*x+b)*(x+d)**2
339
V_ = V_ % U_
340
v1, v0 = V_[1], V_[0]
341
# prepare symmetric functions
342
s = - U_[1] / U_[2]
343
p = U_[0] / U_[2]
344
# compute Mumford coordinates on E1
345
# Points x1, x2 map to x1^2, x2^2
346
U1 = x**2 - (s*s - 2*p)*x + p**2
347
# y = v1 x + v0 becomes (y - v0)^2 = v1^2 x^2
348
# so 2v0 y-v0^2 = p1 - v1^2 xH^2 = p1 - v1^2 xE1
349
V1 = (p1 - v1**2 * x + v0**2) / (2*v0)
350
# Reduce Mumford coordinates to get a E1 point
351
V1 = V1 % U1
352
U1red = (p1 - V1**2) // U1
353
xP1 = -U1red[0] / U1red[1]
354
yP1 = V1(xP1)
355
assert yP1**2 == p1(xP1)
356
# Same for E2
357
# Points x1, x2 map to 1/x1^2, 1/x2^2
358
U2 = x**2 - (s*s-2*p)/p**2*x + 1/p**2
359
# yE = y1/x1^3, xE = 1/x1^2
360
# means yE = y1 x1 xE^2
361
# (yE - y1 x1 xE^2)(yE - y2 x2 xE^2) = 0
362
# p2 - yE (x1 y1 + x2 y2) xE^2 + (x1 y1 x2 y2 xE^4) = 0
363
V21 = x**2 * (v1 * (s*s-2*p) + v0*s)
364
V20 = p2 + x**4 * (p*(v1**2*p + v1*v0*s + v0**2))
365
# V21 * y = V20
366
_, V21inv, _ = V21.xgcd(U2)
367
V2 = (V21inv * V20) % U2
368
# assert V2**2 % U2 == p2 % U2
369
# Reduce coordinates
370
U2red = (p2 - V2**2) // U2
371
xP2 = -U2red[0] / U2red[1]
372
yP2 = V2(xP2)
373
# assert yP2**2 == p2(xP2)
374
375
return E1(morphE1(xP1, yP1)), E2(morphE2(xP2, yP2))
376
377
return isogeny, (E1, E2)
378
379
def Does22ChainSplit(C, E, P_c, Q_c, P, Q, a):
380
"""
381
Returns None if the chain does not split
382
or a tuple (chain of isogenies, codomain (E1, E2))
383
"""
384
chain = []
385
# gluing step
386
h, D11, D12, D21, D22, f = FromProdToJac(C, E, P_c, Q_c, P, Q, a)
387
chain.append(f)
388
next_powers = None
389
# print(f"order 2^{a-1} on hyp curve ...")
390
for i in range(1,a-2+1):
391
h, D11, D12, D21, D22, f, next_powers = FromJacToJac(
392
h, D11, D12, D21, D22, a-i, powers=next_powers)
393
chain.append(f)
394
395
# now we are left with a quadratic splitting: is it singular?
396
G1 = D11
397
G2 = D21
398
G3, r3 = h.quo_rem(G1 * G2)
399
assert r3 == 0
400
401
delta = Matrix(G.padded_list(3) for G in (G1,G2,G3))
402
if delta.determinant():
403
# Determinant is non-zero, no splitting
404
return None
405
406
# Splitting found!
407
# Finish chain
408
f, codomain = FromJacToProd(G1, G2, G3)
409
chain.append(f)
410
return chain, codomain
411
412
413
def Pushing3Chain(E, P, i):
414
"""
415
Compute chain of isogenies quotienting
416
out a point P of order 3^i
417
418
https://trac.sagemath.org/ticket/34239
419
"""
420
def rec(Q, k):
421
assert k
422
if k == 1: # base case
423
# assert Q and not 3*Q
424
return [EllipticCurveIsogeny(Q.curve(), Q, degree=3, check=False)]
425
426
k1 = int(k * .8 + .5)
427
k1 = max(1, min(k-1, k1)) # clamp to [1, k-1]
428
429
Q1 = 3**k1 * Q
430
L = rec(Q1, k-k1)
431
432
Q2 = Q
433
for psi in L:
434
Q2 = psi(Q2)
435
R = rec(Q2, k1)
436
437
return L + R
438
439
chain = rec(P, i)
440
return chain[-1].codomain(), chain
441
442
def AuxiliaryIsogeny(i, u, v, E_start, P2, Q2, tauhatkernel, two_i):
443
"""
444
Compute the distored kernel using precomputed u,v and the
445
automorphism two_i.
446
447
This is used to construct the curve C from E_start and we
448
compute the image of the points P_c and Q_c
449
"""
450
tauhatkernel_distort = u*tauhatkernel + v*two_i(tauhatkernel)
451
452
C, tau_tilde = Pushing3Chain(E_start, tauhatkernel_distort, i)
453
def chain(P):
454
Pc = u*P + v*two_i(P)
455
for taut in tau_tilde:
456
Pc = taut(Pc)
457
return Pc
458
return C, chain(P2), chain(Q2), chain
459
460