-
Notifications
You must be signed in to change notification settings - Fork 578
/
ipa_commitments.py
256 lines (223 loc) · 10.6 KB
/
ipa_commitments.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
from py_ecc import optimized_bls12_381 as b
from hashlib import sha256
from dataclasses import dataclass
from multicombs import lincomb
import time
# See page 25 and 29 of https://eprint.iacr.org/2020/1536.pdf and
# page 49-50 of https://eprint.iacr.org/2020/499.pdf
# ----------------------------------------------------------------------- #
# THIS IS AN EDUCATIONAL IMPLEMENTATION ONLY. DO NOT USE IN PRODUCTION!!! #
# ----------------------------------------------------------------------- #
BLS12_381_COFACTOR = 76329603384216526031706109802092473003
def hash(x):
return sha256(x).digest()
# Creates the generator points. This is a public procedure that can be repeated
# by anyone, so it is NOT a trusted setup
def mk_generator_points(count):
points = []
x = b.FQ(1)
while len(points) < count:
y = (x ** 3 + b.b) ** ((b.field_modulus + 1) // 4)
if b.is_on_curve((x, y, b.FQ(1)), b.b):
points.append(b.multiply((x, y, b.FQ(1)), BLS12_381_COFACTOR))
x += b.FQ(1)
return points
# Commit to some polynomial
def commit(generator_points, poly):
# Equivalent (but faster) to this:
# reduce(
# b.add,
# [b.multiply(pt, cf) for pt, cf in zip(generator_points[:len(poly)], poly)],
# b.Z1
# )
return lincomb(generator_points[:len(poly)], poly, b.add, b.Z1)
# Returne True iff x is a power of two
def is_power_of_two(x):
return x and (x & (x-1) == 0)
# Serializes an elliptic curve point. Used for Fiat-Shamir.
def serialize_point(pt):
pt = b.normalize(pt)
return pt[0].n.to_bytes(64, 'little') + pt[1].n.to_bytes(64, 'little')
# Returns the (left|right) half of something
def left_half(x):
return x[:len(x)//2]
def right_half(x):
return x[len(x)//2:]
# The data structure for a proof
@dataclass
class Proof():
L: list
R: list
tip: int
# Prove that `commitment` actually is the commitment to a polynomial
# (it does not prove _which_ polynomial)
def prove(points, commitment, poly):
assert is_power_of_two(len(poly))
# Crop the base points to just what we need
points = points[:len(poly)]
# Left-side points for the proof
L = []
# Right-side points for the proof
R = []
# Fiat-shamir randomness value
r = hash(serialize_point(commitment))
# log(n) rounds...
while len(poly) > 1:
# Generate the left-side and right-side points
polyL, polyR = left_half(poly), right_half(poly)
pointsL, pointsR = left_half(points), right_half(points)
yL = commit(pointsR, polyL)
yR = commit(pointsL, polyR)
L.append(yL)
R.append(yR)
# Generate random coefficient for recombining the L and R and commitment
r = hash(r + serialize_point(yL) + serialize_point(yR))
a = int.from_bytes(r, 'little') % b.curve_order
# print('a value: ', a)
# Generate half-size polynomial and points for the next round
poly = [(cL + cR * a) % b.curve_order for (cL, cR) in zip(polyL, polyR)]
points = [b.add(b.multiply(pL, a), pR) for (pL, pR) in zip(pointsL, pointsR)]
# print('intermediate commitment:', commit(points, poly))
return Proof(L, R, poly[0])
def verify(points, commitment, proof):
# Crop the base points to just what we need
points = points[:2**len(proof.L)]
# Fiat-shamir randomness value
r = hash(serialize_point(commitment))
# For verification, we need to generate the same random linear combination of
# base points that the prover did.. But because we don't need to use it until
# the end, we do it more efficiently here: when we progress through the rounds,
# we keep track of how many times each points[i] will appear in the final
# result...
points_coeffs = [1]
# log(n) rounds, just like the prover...
for i in range(len(proof.L)):
r = hash(r + serialize_point(proof.L[i]) + serialize_point(proof.R[i]))
# Generate random coefficient for recombining (same as the prover)
a = int.from_bytes(r, 'little') % b.curve_order
# print('a value: ', a)
# Add L and R into the commitment, applying the appropriate coefficients
commitment = b.add(
proof.L[i],
b.add(
b.multiply(commitment, a),
b.multiply(proof.R[i], a**2)
)
)
# print('intermediate commitment:', commitment)
# Update the coefficients (points_coeffs[i] = how many times points[i] will
# appear in the single base point of the last round)
points_coeffs = sum([[(x*a) % b.curve_order, x] for x in points_coeffs], [])
# Finally, we do the linear combination
combined_point = lincomb(points, points_coeffs, b.add, b.Z1)
# Base case check: base_point * coefficient ?= commitment
return b.eq(b.multiply(combined_point, proof.tip), commitment)
# Prove that `commitment` actually is the commitment to a polynomial
# `p` such that `p(x) = y`
def prove_evaluation(points, commitment, poly, x, y):
assert is_power_of_two(len(poly))
# Crop the base points to just what we need. We add an additional base point,
# which we will use to mix in the _evaluation_ of the polynomial.
points, H = points[:len(poly)], points[len(poly)]
# Alongside the base points, we track the powers of the x coordinate we are
# proving an evaluation for. These points get manipulated in the same way as the
# base points do.
xpowers = [pow(x, i, b.curve_order) for i in range(len(poly))]
# Left-side points for the proof
L = []
# Right-side points for the proof
R = []
# Fiat-shamir randomness value
r = hash(serialize_point(commitment) + x.to_bytes(32, 'little') + y.to_bytes(32, 'little'))
# For security, we randomize H
H = b.multiply(H, int.from_bytes(r, 'little') % b.curve_order)
while len(poly) > 1:
# Generate the left-side and right-side points, except we also mix in a similarly
# constructed "commitment" that uses `H * powers of x` as its base instead of the
# base points.
polyL, polyR = left_half(poly), right_half(poly)
pointsL, pointsR = left_half(points), right_half(points)
xpowersL, xpowersR = left_half(xpowers), right_half(xpowers)
yL = commit(pointsR, polyL)
yR = commit(pointsL, polyR)
L.append(b.add(yL, b.multiply(H, sum(a*b for a,b in zip(xpowersR, polyL)))))
R.append(b.add(yR, b.multiply(H, sum(a*b for a,b in zip(xpowersL, polyR)))))
# Generate random coefficient for recombining the L and R and commitment
r = hash(r + serialize_point(L[-1]) + serialize_point(R[-1]))
a = int.from_bytes(r, 'little') % b.curve_order
# print('a value: ', a)
# Generate half-size polynomial and points for the next round. Notice how we treat
# the powers of x the same way that we do the base points
poly = [(cL + cR * a) % b.curve_order for (cL, cR) in zip(polyL, polyR)]
points = [b.add(b.multiply(pL, a), pR) for (pL, pR) in zip(pointsL, pointsR)]
xpowers = [(xL * a + xR) % b.curve_order for (xL, xR) in zip(xpowersL, xpowersR)]
# print('intermediate commitment:', b.add(commit(points, poly), b.multiply(H, sum(a*b for a,b in zip(xpowers, poly)))))
return Proof(L, R, poly[0])
# Verify a proof of an evaluation made using the above protocol
def verify_evaluation(points, commitment, proof, x, y):
# Crop the base points to just what we need. We add an additional base point,
# which we will use to mix in the _evaluation_ of the polynomial.
points, H = points[:2**len(proof.L)], points[2**len(proof.L)]
# Powers of x, as in the prover
xpowers = [pow(x, i, b.curve_order) for i in range(len(poly))]
# Fiat-shamir randomness value
r = hash(serialize_point(commitment) + x.to_bytes(32, 'little') + y.to_bytes(32, 'little'))
# For security, we randomize H
H = b.multiply(H, int.from_bytes(r, 'little') % b.curve_order)
# We "mix in" H * the claimed evaluation P(x) = y. Notice that `H * P(x)` equals the
# dot-product of `H * powers of x` and the polynomial coefficients, so it has the
# "same format" as the polynomial commitment itself. This allows us to verify the
# evaluation using the same technique that we use to just prove that the commitment
# is valid
commitment = b.add(commitment, b.multiply(H, y))
# Track the linear combination so we can generate the final-round point and xpower,
# just as before
points_coeffs = [1]
for i in range(len(proof.L)):
# Generate random coefficient for recombining (same as the prover)
r = hash(r + serialize_point(proof.L[i]) + serialize_point(proof.R[i]))
a = int.from_bytes(r, 'little') % b.curve_order
# print('a value: ', a)
# Add L and R into the commitment, applying the appropriate coefficients
commitment = b.add(
proof.L[i],
b.add(
b.multiply(commitment, a),
b.multiply(proof.R[i], a**2)
)
)
# print('intermediate commitment:', commitment)
# Update the coefficients (as in basic verification above)
points_coeffs = sum([[(x*a) % b.curve_order, x] for x in points_coeffs], [])
# Finally, we do the linear combination; same one for base points and x powers
combined_point = lincomb(points, points_coeffs, b.add, b.Z1)
combined_x_powers = sum(p*c for p,c in zip(xpowers, points_coeffs))
# Base case check: base_point * coefficient ?= commitment. Note that here we
# have to also mix H * the combined xpower into the final base point
return b.eq(
b.add(
b.multiply(combined_point, proof.tip),
b.multiply(H, (proof.tip * combined_x_powers) % b.curve_order)
),
commitment
)
time_cache = [time.time()]
def get_time_delta():
time_cache.append(time.time())
return time_cache[-1] - time_cache[-2]
if __name__ == '__main__':
get_time_delta()
points = mk_generator_points(32)
print("Generated generator points: {:.3f}s".format(get_time_delta()))
poly = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3]
commitment = commit(points, poly)
print("Simple commitment generated: {:.3f}s".format(get_time_delta()))
proof = prove(points, commitment, poly)
print("Proof generated: {:.3f}s".format(get_time_delta()))
print(proof)
assert verify(points, commitment, proof)
print("Proof verified: {:.3f}s".format(get_time_delta()))
proof2 = prove_evaluation(points, commitment, poly, 10, 3979853562951413)
print("Evaluation proof generated: {:.3f}s".format(get_time_delta()))
assert verify_evaluation(points, commitment, proof2, 10, 3979853562951413)
print("Evaluation proof verified: {:.3f}s".format(get_time_delta()))