github.com/luckypickle/go-ethereum-vet@v1.14.2/crypto/secp256k1/libsecp256k1/sage/group_prover.sage (about) 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)