plonk circuit setup routines

This commit is contained in:
narodnik
2021-06-23 09:48:22 +02:00
parent 028bbe1dce
commit 091184f005
14 changed files with 801 additions and 334 deletions

View File

@@ -32,4 +32,3 @@ def extendedEuclideanAlgorithm(a, b):
a, b, x2, x1, y2, y1 = b, r, x1, x, y1, y
return (x2, y2, a)

View File

@@ -1,6 +1,6 @@
import random
from polynomial import polynomialsOver
from modp import *
from .polynomial import polynomialsOver
from .modp import *

View File

@@ -10,4 +10,3 @@ test(mod7(3), mod7(3) * 1)
test(mod7(2), mod7(5) + mod7(4))
test(True, mod7(0) == mod7(3) + mod7(4))

View File

@@ -16,7 +16,8 @@ def IntegersModP(p):
try:
self.n = int(n) % IntegerModP.p
except:
raise TypeError("Can't cast type %s to %s in __init__" % (type(n).__name__, type(self).__name__))
raise TypeError("Can't cast type %s to %s in __init__" %
(type(n).__name__, type(self).__name__))
self.field = IntegerModP
@@ -70,6 +71,9 @@ def IntegersModP(p):
def __int__(self):
return self.n
def __hash__(self):
return hash((self.n, self.p))
IntegerModP.p = p
IntegerModP.__name__ = 'Z/%d' % (p)
IntegerModP.englishName = 'IntegersMod%d' % (p)

View File

@@ -95,4 +95,3 @@ class FieldElement(DomainElement):
def __rtruediv__(self, other): return self.inverse() * other
def __div__(self, other): return self.__truediv__(other)
def __rdiv__(self, other): return self.__rtruediv__(other)

View File

@@ -121,6 +121,17 @@ def polynomialsOver(field=fractions.Fraction):
raise ZeroDivisionError
return divmod(self, divisor)[1]
def __call__(self, x):
if type(x) is int:
x = self.field(x)
assert type(x) is self.field
if self.isZero():
return self.field(0)
y = self.leadingCoefficient()
for coeff in self.coefficients[-2::-1]:
y = y * x + coeff
return y
def Zero():
return Polynomial([])
@@ -130,4 +141,3 @@ def polynomialsOver(field=fractions.Fraction):
Polynomial.__name__ = '(%s)[x]' % field.__name__
Polynomial.englishName = 'Polynomials in one variable over %s' % field.__name__
return Polynomial

View File

@@ -6,4 +6,3 @@ def test(expected, actual):
(code, lineno, filename, expected, actual))
sys.exit(1)

View File

@@ -9,4 +9,3 @@ p = Polynomial([1,2])
x+p
p+x

33
scripts/halo/misc.py Normal file
View File

@@ -0,0 +1,33 @@
import random
def sample_random(fp, seed):
rnd = random.Random(seed)
# Range of the field is 0 ... p - 1
return fp(rnd.randint(0, fp.p - 1))
def is_power_of_two(n):
# Power of two number is represented by a single digit
# followed by zeroes.
return n & (n - 1) == 0
#| ## Choosing roots of unity
def get_omega(fp, n, seed=None):
"""
Given a field, this method returns an n^th root of unity.
If the seed is not None then this method will return the
same n'th root of unity for every run with the same seed
This only makes sense if n is a power of 2.
"""
assert is_power_of_two(n)
# https://crypto.stackexchange.com/questions/63614/finding-the-n-th-root-of-unity-in-a-finite-field
while True:
# Sample random x != 0
x = sample_random(fp, seed)
# Compute g = x^{(q - 1)/n}
y = pow(x, (fp.p - 1) // n)
# If g^{n/2} != 1 then g is a primitive root
if y != 1 and pow(y, n // 2) != 1:
assert pow(y, n) == 1, "omega must be 2nd root of unity"
return y

5
scripts/halo/pasta.py Normal file
View File

@@ -0,0 +1,5 @@
from finite_fields import finitefield
p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001
fp = finitefield.IntegersModP(p)

View File

@@ -0,0 +1,301 @@
#| # Evaluation Representation of Polynomials and FFT optimizations
#| In addition to the coefficient-based representation of polynomials used
#| in babysnark.py, for performance we will also use an alternative
#| representation where the polynomial is evaluated at a fixed set of points.
#| Some operations, like multiplication and division, are significantly more
#| efficient in this form.
#| We can use FFT-based tools for efficiently converting
#| between coefficient and evaluation representation.
#|
#| This library provides:
#| - Fast fourier transform for finite fields
#| - Interpolation and evaluation using FFT
from finite_fields.finitefield import FiniteField
from finite_fields.polynomial import polynomialsOver
from finite_fields.euclidean import extendedEuclideanAlgorithm
import random
from finite_fields.numbertype import typecheck, memoize, DomainElement
from functools import reduce
import numpy as np
#| ## Fast Fourier Transform on Finite Fields
def fft_helper(a, omega, field):
"""
Given coefficients A of polynomial this method does FFT and returns
the evaluation of the polynomial at [omega^0, omega^(n-1)]
If the polynomial is a0*x^0 + a1*x^1 + ... + an*x^n then the coefficients
list is of the form [a0, a1, ... , an].
"""
n = len(a)
assert not (n & (n - 1)), "n must be a power of 2"
if n == 1:
return a
b, c = a[0::2], a[1::2]
b_bar = fft_helper(b, pow(omega, 2), field)
c_bar = fft_helper(c, pow(omega, 2), field)
a_bar = [field(1)] * (n)
for j in range(n):
k = j % (n // 2)
a_bar[j] = b_bar[k] + pow(omega, j) * c_bar[k]
return a_bar
#| ## Representing a polynomial by evaluation at fixed points
@memoize
def make_polynomial_evalrep(field, omega, n):
assert n & n - 1 == 0, "n must be a power of 2"
# Check that omega is an n'th primitive root of unity
assert type(omega) is field
omega = field(omega)
assert omega**(n) == 1
_powers = [omega**i for i in range(n)]
assert len(set(_powers)) == n
_poly_coeff = polynomialsOver(field)
class PolynomialEvalRep(object):
def __init__(self, xs, ys):
# Each element of xs must be a power of omega.
# There must be a corresponding y for every x.
if type(xs) is not tuple:
xs = tuple(xs)
if type(ys) is not tuple:
ys = tuple(ys)
assert len(xs) <= n+1
assert len(xs) == len(ys)
for x in xs:
assert x in _powers
for y in ys:
assert type(y) is field
self.evalmap = dict(zip(xs, ys))
@classmethod
def from_coeffs(cls, poly):
assert type(poly) is _poly_coeff
assert poly.degree() <= n
padded_coeffs = poly.coefficients + [field(0)] * (n - len(poly.coefficients))
ys = fft_helper(padded_coeffs, omega, field)
xs = [omega**i for i in range(n) if ys[i] != 0]
ys = [y for y in ys if y != 0]
return cls(xs, ys)
def to_coeffs(self):
# To convert back to the coefficient form, we use polynomial interpolation.
# The non-zero elements stored in self.evalmap, so we fill in the zero values
# here.
ys = [self.evalmap[x] if x in self.evalmap else field(0) for x in _powers]
coeffs = [b / field(n) for b in fft_helper(ys, 1 / omega, field)]
return _poly_coeff(coeffs)
_lagrange_cache = {}
def __call__(self, x):
if type(x) is int:
x = field(x)
assert type(x) is field
xs = _powers
def lagrange(x, xi):
# Let's cache lagrange values
if (x,xi) in PolynomialEvalRep._lagrange_cache:
return PolynomialEvalRep._lagrange_cache[(x,xi)]
mul = lambda a,b: a*b
num = reduce(mul, [x - xj for xj in xs if xj != xi], field(1))
den = reduce(mul, [xi - xj for xj in xs if xj != xi], field(1))
PolynomialEvalRep._lagrange_cache[(x,xi)] = num / den
return PolynomialEvalRep._lagrange_cache[(x,xi)]
y = field(0)
for xi, yi in self.evalmap.items():
y += yi * lagrange(x, xi)
return y
def __mul__(self, other):
# Scale by integer
if type(other) is int:
other = field(other)
if type(other) is field:
return PolynomialEvalRep(self.evalmap.keys(),
[other * y for y in self.evalmap.values()])
# Multiply another polynomial in the same representation
if type(other) is type(self):
xs = []
ys = []
for x, y in self.evalmap.items():
if x in other.evalmap:
xs.append(x)
ys.append(y * other.evalmap[x])
return PolynomialEvalRep(xs, ys)
@typecheck
def __iadd__(self, other):
# Add another polynomial to this one in place.
# This is especially efficient when the other polynomial is sparse,
# since we only need to add the non-zero elements.
for x, y in other.evalmap.items():
if x not in self.evalmap:
self.evalmap[x] = y
else:
self.evalmap[x] += y
return self
@typecheck
def __add__(self, other):
res = PolynomialEvalRep(self.evalmap.keys(), self.evalmap.values())
res += other
return res
def __sub__(self, other): return self + (-other)
def __neg__(self): return PolynomialEvalRep(self.evalmap.keys(),
[-y for y in self.evalmap.values()])
def __truediv__(self, divisor):
# Scale by integer
if type(divisor) is int:
other = field(divisor)
if type(divisor) is field:
return self * (1/divisor)
if type(divisor) is type(self):
res = PolynomialEvalRep((),())
for x, y in self.evalmap.items():
assert x in divisor.evalmap
res.evalmap[x] = y / divisor.evalmap[x]
return res
return NotImplemented
def __copy__(self):
return PolynomialEvalRep(self.evalmap.keys(), self.evalmap.values())
def __repr__(self):
return f'PolyEvalRep[{hex(omega.n)[:15]}...,{n}]({len(self.evalmap)} elements)'
@classmethod
def divideWithCoset(cls, p, t, c=field(3)):
"""
This assumes that p and t are polynomials in coefficient representation,
and that p is divisible by t.
This function is useful when t has roots at some or all of the powers of omega,
in which case we cannot just convert to evalrep and use division above
(since it would cause a divide by zero.
Instead, we evaluate p(X) at powers of (c*omega) for some constant cofactor c.
To do this efficiently, we create new polynomials, pc(X) = p(cX), tc(X) = t(cX),
and evaluate these at powers of omega. This conversion can be done efficiently
on the coefficient representation.
See also: cosetFFT in libsnark / libfqfft.
https://github.com/scipr-lab/libfqfft/blob/master/libfqfft/evaluation_domain/domains/extended_radix2_domain.tcc
"""
assert type(p) is _poly_coeff
assert type(t) is _poly_coeff
# Compute p(cX), t(cX) by multiplying coefficients
c_acc = field(1)
pc = _poly_coeff(list(p.coefficients)) # make a copy
for i in range(p.degree() + 1):
pc.coefficients[-i-1] *= c_acc
c_acc *= c
c_acc = field(1)
tc = _poly_coeff(list(t.coefficients)) # make a copy
for i in range(t.degree() + 1):
tc.coefficients[-i-1] *= c_acc
c_acc *= c
# Divide using evalrep
pc_rep = cls.from_coeffs(pc)
tc_rep = cls.from_coeffs(tc)
hc_rep = pc_rep / tc_rep
hc = hc_rep.to_coeffs()
# Compute h(X) from h(cX) by dividing coefficients
c_acc = field(1)
h = _poly_coeff(list(hc.coefficients)) # make a copy
for i in range(hc.degree() + 1):
h.coefficients[-i-1] /= c_acc
c_acc *= c
# Correctness checks
# assert pc == tc * hc
# assert p == t * h
return h
return PolynomialEvalRep
#| ## Sparse Matrix
#| In our setting, we have O(m*m) elements in the matrix, and expect the number of
#| elements to be O(m).
#| In this setting, it's appropriate to use a rowdict representation - a dense
#| array of dictionaries, one for each row, where the keys of each dictionary
#| are column indices.
class RowDictSparseMatrix():
# Only a few necessary methods are included here.
# This could be replaced with a generic sparse matrix class, such as scipy.sparse,
# but this does not work as well with custom value types like Fp
def __init__(self, m, n, zero=None):
self.m = m
self.n = n
self.shape = (m,n)
self.zero = zero
self.rowdicts = [dict() for _ in range(m)]
def __setitem__(self, key, v):
i, j = key
self.rowdicts[i][j] = v
def __getitem__(self, key):
i, j = key
return self.rowdicts[i][j] if j in self.rowdicts[i] else self.zero
def items(self):
for i in range(self.m):
for j, v in self.rowdicts[i].items():
yield (i,j), v
def dot(self, other):
if isinstance(other, np.ndarray):
assert other.dtype == 'O'
assert other.shape in ((self.n,),(self.n,1))
ret = np.empty((self.m,), dtype='O')
ret.fill(self.zero)
for i in range(self.m):
for j, v in self.rowdicts[i].items():
ret[i] += other[j] * v
return ret
def to_dense(self):
mat = np.empty((self.m, self.n), dtype='O')
mat.fill(self.zero)
for (i,j), val in self.items():
mat[i,j] = val
return mat
def __repr__(self): return repr(self.rowdicts)
#-
# Examples
if __name__ == '__main__':
import misc
Fp = FiniteField(52435875175126190479447740508185965837690552500527637822603658699938581184513,1) # (# noqa: E501)
Poly = polynomialsOver(Fp)
n = 8
omega = misc.get_omega(Fp, n)
PolyEvalRep = make_polynomial_evalrep(Fp, omega, n)
f = Poly([1,2,3,4,5])
xs = tuple([omega**i for i in range(n)])
ys = tuple(map(f, xs))
# print('xs:', xs)
# print('ys:', ys)
assert f == PolyEvalRep(xs, ys).to_coeffs()

119
scripts/halo/test.py Normal file
View File

@@ -0,0 +1,119 @@
import sys
sys.path.append("..")
import random
import misc
import pasta
from polynomial_evalrep import make_polynomial_evalrep
n = 8
omega_base = misc.get_omega(pasta.fp, 2**32, seed=0)
assert misc.is_power_of_two(8)
omega = omega_base ** (2 ** 32 // n)
# Order of omega is n
assert omega ** n == 1
# Compute complete roots of this group
ROOTS = [omega ** i for i in range(n)]
PolyEvalRep = make_polynomial_evalrep(pasta.fp, omega, n)
import numpy as np
from tabulate import tabulate
# Wires
a = ["x", "v1", "v2", "1", "1", "v3", "e1", "e2"]
b = ["x", "x", "x", "5", "35", "5", "e3", "e4"]
c = ["v1", "v2", "v3", "5", "35", "35", "e5", "e6"]
wires = a + b + c
# Gates
# La + Rb + Oc + Mab + C = 0
add = np.array([1, 1, 0, -1, 0])
mul = np.array([0, 0, 1, -1, 0])
const5 = np.array([0, 1, 0, 0, -5])
public_input = np.array([0, 1, 0, 0, 0])
empty = np.array([0, 0, 0, 0, 0])
gates_matrix = np.array(
[mul, mul, add, const5, public_input, add, empty, empty])
print("Wires:")
print(tabulate([["a ="] + a, ["b ="] + b, ["c ="] + c]))
print()
print("Gates:")
print(gates_matrix)
print()
# The index of the public input in the gates_matrix
# We specify its position and its value
public_input_values = [(4, 35)]
def permute_indices(wires):
size = len(wires)
permutation = [i + 1 for i in range(size)]
for i in range(size):
for j in range(i + 1, size):
if wires[i] == wires[j]:
permutation[i], permutation[j] = permutation[j], permutation[i]
break
return permutation
permutation = permute_indices(wires)
table = [
["Wires"] + wires,
["Indices"] + list(i + 1 for i in range(len(wires))),
["Permutations"] + permutation
]
print(tabulate(table))
print()
import misc
from pasta import fp
def setup(wires, gates_matrix):
# Section 8.1
# The selector polynomials that define the circuit's arithmetisation
gates_matrix = gates_matrix.transpose()
ql = PolyEvalRep(ROOTS, [fp(i) for i in gates_matrix[0]])
qr = PolyEvalRep(ROOTS, [fp(i) for i in gates_matrix[1]])
qm = PolyEvalRep(ROOTS, [fp(i) for i in gates_matrix[2]])
qo = PolyEvalRep(ROOTS, [fp(i) for i in gates_matrix[3]])
qc = PolyEvalRep(ROOTS, [fp(i) for i in gates_matrix[4]])
selector_polys = [ql, qr, qm, qo, qc]
public_input = [fp(0) for i in range(len(ROOTS))]
for (index, value) in public_input_values:
# This is negative because the value is added to
# the output of the const selector poly:
# La + Rb + Oc + Mab + (C + PI) = 0
public_input[index] = fp(-value)
public_input_poly = PolyEvalRep(ROOTS, public_input)
# Identity permutations applied to a, b, c
# Ideally H, k_1 H, k_2 H are distinct cosets of H
# Here we just sample k and assume it's high-order
# Random high order k to form distinct cosets
k = misc.sample_random(fp)
id_domain_a = ROOTS
id_domain_b = [k * root for root in ROOTS]
id_domain_c = [k**2 * root for root in ROOTS]
id_domain = id_domain_a + id_domain_b + id_domain_c
# Intermediate step where we permute the positions of the domain
# generated above
permuted_domain = [id_domain[i - 1] for i in permutation]
permuted_domain_a = permuted_domain[:n]
permuted_domain_b = permuted_domain[n:2 * n]
permuted_domain_c = permuted_domain[2*n:3 * n]
# The copy permuation applied to a, b, c
# Returns the permuted index value (corresponding root of unity coset)
# when evaluated on the domain.
ssigma_1 = PolyEvalRep(ROOT, permuted_domain_a)
ssigma_2 = PolyEvalRep(ROOT, permuted_domain_b)
ssigma_3 = PolyEvalRep(ROOT, permuted_domain_c)
copy_permutes = [ssigma_1, ssigma_2, ssigma_3]
setup(wires, gates_matrix)