]> Git Repo - secp256k1.git/blob - sage/group_prover.sage
ab580c5b23bbf786f41757b8a3800958db37c7a7
[secp256k1.git] / sage / group_prover.sage
1 # This code supports verifying group implementations which have branches
2 # or conditional statements (like cmovs), by allowing each execution path
3 # to independently set assumptions on input or intermediary variables.
4 #
5 # The general approach is:
6 # * A constraint is a tuple of two sets of of symbolic expressions:
7 #   the first of which are required to evaluate to zero, the second of which
8 #   are required to evaluate to nonzero.
9 #   - A constraint is said to be conflicting if any of its nonzero expressions
10 #     is in the ideal with basis the zero expressions (in other words: when the
11 #     zero expressions imply that one of the nonzero expressions are zero).
12 # * There is a list of laws that describe the intended behaviour, including
13 #   laws for addition and doubling. Each law is called with the symbolic point
14 #   coordinates as arguments, and returns:
15 #   - A constraint describing the assumptions under which it is applicable,
16 #     called "assumeLaw"
17 #   - A constraint describing the requirements of the law, called "require"
18 # * Implementations are transliterated into functions that operate as well on
19 #   algebraic input points, and are called once per combination of branches
20 #   exectured. Each execution returns:
21 #   - A constraint describing the assumptions this implementation requires
22 #     (such as Z1=1), called "assumeFormula"
23 #   - A constraint describing the assumptions this specific branch requires,
24 #     but which is by construction guaranteed to cover the entire space by
25 #     merging the results from all branches, called "assumeBranch"
26 #   - The result of the computation
27 # * All combinations of laws with implementation branches are tried, and:
28 #   - If the combination of assumeLaw, assumeFormula, and assumeBranch results
29 #     in a conflict, it means this law does not apply to this branch, and it is
30 #     skipped.
31 #   - For others, we try to prove the require constraints hold, assuming the
32 #     information in assumeLaw + assumeFormula + assumeBranch, and if this does
33 #     not succeed, we fail.
34 #     + To prove an expression is zero, we check whether it belongs to the
35 #       ideal with the assumed zero expressions as basis. This test is exact.
36 #     + To prove an expression is nonzero, we check whether each of its
37 #       factors is contained in the set of nonzero assumptions' factors.
38 #       This test is not exact, so various combinations of original and
39 #       reduced expressions' factors are tried.
40 #   - If we succeed, we print out the assumptions from assumeFormula that
41 #     weren't implied by assumeLaw already. Those from assumeBranch are skipped,
42 #     as we assume that all constraints in it are complementary with each other.
43 #
44 # Based on the sage verification scripts used in the Explicit-Formulas Database
45 # by Tanja Lange and others, see http://hyperelliptic.org/EFD
46
47 class fastfrac:
48   """Fractions over rings."""
49
50   def __init__(self,R,top,bot=1):
51     """Construct a fractional, given a ring, a numerator, and denominator."""
52     self.R = R
53     if parent(top) == ZZ or parent(top) == R:
54       self.top = R(top)
55       self.bot = R(bot)
56     elif top.__class__ == fastfrac:
57       self.top = top.top
58       self.bot = top.bot * bot
59     else:
60       self.top = R(numerator(top))
61       self.bot = R(denominator(top)) * bot
62
63   def iszero(self,I):
64     """Return whether this fraction is zero given an ideal."""
65     return self.top in I and self.bot not in I
66
67   def reduce(self,assumeZero):
68     zero = self.R.ideal(map(numerator, assumeZero))
69     return fastfrac(self.R, zero.reduce(self.top)) / fastfrac(self.R, zero.reduce(self.bot))
70
71   def __add__(self,other):
72     """Add two fractions."""
73     if parent(other) == ZZ:
74       return fastfrac(self.R,self.top + self.bot * other,self.bot)
75     if other.__class__ == fastfrac:
76       return fastfrac(self.R,self.top * other.bot + self.bot * other.top,self.bot * other.bot)
77     return NotImplemented
78
79   def __sub__(self,other):
80     """Subtract two fractions."""
81     if parent(other) == ZZ:
82       return fastfrac(self.R,self.top - self.bot * other,self.bot)
83     if other.__class__ == fastfrac:
84       return fastfrac(self.R,self.top * other.bot - self.bot * other.top,self.bot * other.bot)
85     return NotImplemented
86
87   def __neg__(self):
88     """Return the negation of a fraction."""
89     return fastfrac(self.R,-self.top,self.bot)
90
91   def __mul__(self,other):
92     """Multiply two fractions."""
93     if parent(other) == ZZ:
94       return fastfrac(self.R,self.top * other,self.bot)
95     if other.__class__ == fastfrac:
96       return fastfrac(self.R,self.top * other.top,self.bot * other.bot)
97     return NotImplemented
98
99   def __rmul__(self,other):
100     """Multiply something else with a fraction."""
101     return self.__mul__(other)
102
103   def __div__(self,other):
104     """Divide two fractions."""
105     if parent(other) == ZZ:
106       return fastfrac(self.R,self.top,self.bot * other)
107     if other.__class__ == fastfrac:
108       return fastfrac(self.R,self.top * other.bot,self.bot * other.top)
109     return NotImplemented
110
111   def __pow__(self,other):
112     """Compute a power of a fraction."""
113     if parent(other) == ZZ:
114       if other < 0:
115         # Negative powers require flipping top and bottom
116         return fastfrac(self.R,self.bot ^ (-other),self.top ^ (-other))
117       else:
118         return fastfrac(self.R,self.top ^ other,self.bot ^ other)
119     return NotImplemented
120
121   def __str__(self):
122     return "fastfrac((" + str(self.top) + ") / (" + str(self.bot) + "))"
123   def __repr__(self):
124     return "%s" % self
125
126   def numerator(self):
127     return self.top
128
129 class constraints:
130   """A set of constraints, consisting of zero and nonzero expressions.
131
132   Constraints can either be used to express knowledge or a requirement.
133
134   Both the fields zero and nonzero are maps from expressions to description
135   strings. The expressions that are the keys in zero are required to be zero,
136   and the expressions that are the keys in nonzero are required to be nonzero.
137
138   Note that (a != 0) and (b != 0) is the same as (a*b != 0), so all keys in
139   nonzero could be multiplied into a single key. This is often much less
140   efficient to work with though, so we keep them separate inside the
141   constraints. This allows higher-level code to do fast checks on the individual
142   nonzero elements, or combine them if needed for stronger checks.
143
144   We can't multiply the different zero elements, as it would suffice for one of
145   the factors to be zero, instead of all of them. Instead, the zero elements are
146   typically combined into an ideal first.
147   """
148
149   def __init__(self, **kwargs):
150     if 'zero' in kwargs:
151       self.zero = dict(kwargs['zero'])
152     else:
153       self.zero = dict()
154     if 'nonzero' in kwargs:
155       self.nonzero = dict(kwargs['nonzero'])
156     else:
157       self.nonzero = dict()
158
159   def negate(self):
160     return constraints(zero=self.nonzero, nonzero=self.zero)
161
162   def __add__(self, other):
163     zero = self.zero.copy()
164     zero.update(other.zero)
165     nonzero = self.nonzero.copy()
166     nonzero.update(other.nonzero)
167     return constraints(zero=zero, nonzero=nonzero)
168
169   def __str__(self):
170     return "constraints(zero=%s,nonzero=%s)" % (self.zero, self.nonzero)
171
172   def __repr__(self):
173     return "%s" % self
174
175
176 def conflicts(R, con):
177   """Check whether any of the passed non-zero assumptions is implied by the zero assumptions"""
178   zero = R.ideal(map(numerator, con.zero))
179   if 1 in zero:
180     return True
181   # First a cheap check whether any of the individual nonzero terms conflict on
182   # their own.
183   for nonzero in con.nonzero:
184     if nonzero.iszero(zero):
185       return True
186   # It can be the case that entries in the nonzero set do not individually
187   # conflict with the zero set, but their combination does. For example, knowing
188   # that either x or y is zero is equivalent to having x*y in the zero set.
189   # Having x or y individually in the nonzero set is not a conflict, but both
190   # simultaneously is, so that is the right thing to check for.
191   if reduce(lambda a,b: a * b, con.nonzero, fastfrac(R, 1)).iszero(zero):
192     return True
193   return False
194
195
196 def get_nonzero_set(R, assume):
197   """Calculate a simple set of nonzero expressions"""
198   zero = R.ideal(map(numerator, assume.zero))
199   nonzero = set()
200   for nz in map(numerator, assume.nonzero):
201     for (f,n) in nz.factor():
202       nonzero.add(f)
203     rnz = zero.reduce(nz)
204     for (f,n) in rnz.factor():
205       nonzero.add(f)
206   return nonzero
207
208
209 def prove_nonzero(R, exprs, assume):
210   """Check whether an expression is provably nonzero, given assumptions"""
211   zero = R.ideal(map(numerator, assume.zero))
212   nonzero = get_nonzero_set(R, assume)
213   expl = set()
214   ok = True
215   for expr in exprs:
216     if numerator(expr) in zero:
217       return (False, [exprs[expr]])
218   allexprs = reduce(lambda a,b: numerator(a)*numerator(b), exprs, 1)
219   for (f, n) in allexprs.factor():
220     if f not in nonzero:
221       ok = False
222   if ok:
223     return (True, None)
224   ok = True
225   for (f, n) in zero.reduce(numerator(allexprs)).factor():
226     if f not in nonzero:
227       ok = False
228   if ok:
229     return (True, None)
230   ok = True
231   for expr in exprs:
232     for (f,n) in numerator(expr).factor():
233       if f not in nonzero:
234         ok = False
235   if ok:
236     return (True, None)
237   ok = True
238   for expr in exprs:
239     for (f,n) in zero.reduce(numerator(expr)).factor():
240       if f not in nonzero:
241         expl.add(exprs[expr])
242   if expl:
243     return (False, list(expl))
244   else:
245     return (True, None)
246
247
248 def prove_zero(R, exprs, assume):
249   """Check whether all of the passed expressions are provably zero, given assumptions"""
250   r, e = prove_nonzero(R, dict(map(lambda x: (fastfrac(R, x.bot, 1), exprs[x]), exprs)), assume)
251   if not r:
252     return (False, map(lambda x: "Possibly zero denominator: %s" % x, e))
253   zero = R.ideal(map(numerator, assume.zero))
254   nonzero = prod(x for x in assume.nonzero)
255   expl = []
256   for expr in exprs:
257     if not expr.iszero(zero):
258       expl.append(exprs[expr])
259   if not expl:
260     return (True, None)
261   return (False, expl)
262
263
264 def describe_extra(R, assume, assumeExtra):
265   """Describe what assumptions are added, given existing assumptions"""
266   zerox = assume.zero.copy()
267   zerox.update(assumeExtra.zero)
268   zero = R.ideal(map(numerator, assume.zero))
269   zeroextra = R.ideal(map(numerator, zerox))
270   nonzero = get_nonzero_set(R, assume)
271   ret = set()
272   # Iterate over the extra zero expressions
273   for base in assumeExtra.zero:
274     if base not in zero:
275       add = []
276       for (f, n) in numerator(base).factor():
277         if f not in nonzero:
278           add += ["%s" % f]
279       if add:
280         ret.add((" * ".join(add)) + " = 0 [%s]" % assumeExtra.zero[base])
281   # Iterate over the extra nonzero expressions
282   for nz in assumeExtra.nonzero:
283     nzr = zeroextra.reduce(numerator(nz))
284     if nzr not in zeroextra:
285       for (f,n) in nzr.factor():
286         if zeroextra.reduce(f) not in nonzero:
287           ret.add("%s != 0" % zeroextra.reduce(f))
288   return ", ".join(x for x in ret)
289
290
291 def check_symbolic(R, assumeLaw, assumeAssert, assumeBranch, require):
292   """Check a set of zero and nonzero requirements, given a set of zero and nonzero assumptions"""
293   assume = assumeLaw + assumeAssert + assumeBranch
294
295   if conflicts(R, assume):
296     # This formula does not apply
297     return None
298
299   describe = describe_extra(R, assumeLaw + assumeBranch, assumeAssert)
300
301   ok, msg = prove_zero(R, require.zero, assume)
302   if not ok:
303     return "FAIL, %s fails (assuming %s)" % (str(msg), describe)
304
305   res, expl = prove_nonzero(R, require.nonzero, assume)
306   if not res:
307     return "FAIL, %s fails (assuming %s)" % (str(expl), describe)
308
309   if describe != "":
310     return "OK (assuming %s)" % describe
311   else:
312     return "OK"
313
314
315 def concrete_verify(c):
316   for k in c.zero:
317     if k != 0:
318       return (False, c.zero[k])
319   for k in c.nonzero:
320     if k == 0:
321       return (False, c.nonzero[k])
322   return (True, None)
This page took 0.033462 seconds and 2 git commands to generate.