mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
plonk circuit setup routines
This commit is contained in:
@@ -32,4 +32,3 @@ def extendedEuclideanAlgorithm(a, b):
|
||||
a, b, x2, x1, y2, y1 = b, r, x1, x, y1, y
|
||||
|
||||
return (x2, y2, a)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import random
|
||||
from polynomial import polynomialsOver
|
||||
from modp import *
|
||||
from .polynomial import polynomialsOver
|
||||
from .modp import *
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ def test(expected, actual):
|
||||
(code, lineno, filename, expected, actual))
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -9,4 +9,3 @@ p = Polynomial([1,2])
|
||||
|
||||
x+p
|
||||
p+x
|
||||
|
||||
|
||||
33
scripts/halo/misc.py
Normal file
33
scripts/halo/misc.py
Normal 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
5
scripts/halo/pasta.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from finite_fields import finitefield
|
||||
|
||||
p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001
|
||||
fp = finitefield.IntegersModP(p)
|
||||
|
||||
301
scripts/halo/polynomial_evalrep.py
Normal file
301
scripts/halo/polynomial_evalrep.py
Normal 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
119
scripts/halo/test.py
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user