Table of contents
This document has been possible thanks to the reviews of @asn-d6 and @oskarth as well as the immense help from @arnaucube and Edu(from PSE) who almost completely derived R1CS&Plonkish -> CCS.
Also thanks to Srinath Setty for his help on any question we've had while reading his paper as well as while elaborating this doc.
Due to NP-completeness equivalence, we can easily translate any R1CS statement and translate it into a CCS statement which is completelly equivalent.
To do so, it contais certain information as:
An R1CS structure
In general, a CCS structure
Some of thesedon't really translate to any property in particular. Indeed they're just parameters that are used to be able to adapt all the arithmetizations into CCS.
A CCS instance
If we re-write this a bit, we get:
Let's actually see how we can translate R1CS into CCS:
To do so, we need to set some parameters:
If we apply these constants to the aformentioned equation of a CCS instance:
Let's actually see a sage example done by @arnaucube:
# R1CS-to-CCS (https://eprint.iacr.org/2023/552) Sage prototype
# utils
def matrix_vector_product(M, v):
n = M.nrows()
r = [F(0)] * n
for i in range(0, n):
for j in range(0, M.ncols()):
r[i] += M[i][j] * v[j]
return r
def hadamard_product(a, b):
n = len(a)
r = [None] * n
for i in range(0, n):
r[i] = a[i] * b[i]
return r
def vec_add(a, b):
n = len(a)
r = [None] * n
for i in range(0, n):
r[i] = a[i] + b[i]
return r
def vec_elem_mul(a, s):
r = [None] * len(a)
for i in range(0, len(a)):
r[i] = a[i] * s
return r
# end of utils
# can use any finite field, using a small one for the example
F = GF(101)
# R1CS matrices for: x^3 + x + 5 = y (example from article
# https://www.vitalik.ca/general/2016/12/10/qap.html )
A = matrix([
[F(0), 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 1, 0],
[5, 0, 0, 0, 0, 1],
])
B = matrix([
[F(0), 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0],
])
C = matrix([
[F(0), 0, 0, 1, 0, 0],
[0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1],
[0, 0, 1, 0, 0, 0],
])
print("R1CS matrices:")
print("A:", A)
print("B:", B)
print("C:", C)
z = [F(1), 3, 35, 9, 27, 30]
print("z:", z)
assert len(z) == A.ncols()
n = A.ncols() # == len(z)
m = A.nrows()
# l = len(io) # not used for now
# check R1CS relation
Az = matrix_vector_product(A, z)
Bz = matrix_vector_product(B, z)
Cz = matrix_vector_product(C, z)
print("\nR1CS relation check (Az ∘ Bz == Cz):", hadamard_product(Az, Bz) == Cz)
assert hadamard_product(Az, Bz) == Cz
# Translate R1CS into CCS:
print("\ntranslate R1CS into CCS:")
# fixed parameters (and n, m, l are direct from R1CS)
t=3
q=2
d=2
S1=[0,1]
S2=[2]
S = [S1, S2]
c0=1
c1=-1
c = [c0, c1]
M = [A, B, C]
print("CCS values:")
print("n: %s, m: %s, t: %s, q: %s, d: %s" % (n, m, t, q, d))
print("M:", M)
print("z:", z)
print("S:", S)
print("c:", c)
# check CCS relation (this is agnostic to R1CS, for any CCS instance)
r = [F(0)] * m
for i in range(0, q):
hadamard_output = [F(1)]*m
for j in S[i]:
hadamard_output = hadamard_product(hadamard_output,
matrix_vector_product(M[j], z))
r = vec_add(r, vec_elem_mul(hadamard_output, c[i]))
print("\nCCS relation check (∑ cᵢ ⋅ ◯ Mⱼ z == 0):", r == [0]*m)
assert r == [0]*m
Output:
Before starting, there are a few new things to notice here:
We define a plonkish instance
Let's actually see how we can translate Plonkish into CCS:
Before starting, there are a few new things to notice here:
Differently from R1CS translation which is direct, we have some work to do which isn't simply set
Note that the vanilla plonk equation is:
Note that the Vanilla Plonk on the top is doing:
After seeing the Plonkish we want to translate, we alreay can infer some things:
To start, we need to define our selectors
With this, we can define our instance
Remember that in the paper, we have 2
If we focus on the Plonkish one, we can see the definition:
Hence, this means we can define
Note that we have also our
Remember that we can see
The next step is to define
With that, we define
The following rust code summarizes the actual algorithm to derive the matrixes for the CCS instance
# The following CCS instance values have been provided by Carlos
# (https://github.com/CPerezz) and Edu (https://github.com/ed255),
# and this sage script was made to check the CCS relation.
T = [
[0, 0, 0, 7, 6, 6, 8, 6],
[1, 1, 1, 7, 6, 6, 8, 6],
[2, 3, 4, 6, 9, 9, 7, 6],
[5, 5, 5, 6, 6, 6, 6, 6]
]
# z_plonkish = [F(1), 0, 1, 2, 3, 10, 42, 0,1,-1,2]
# z_ccs = [F(1), 0, 1, 2, 3, 10, 42, 1]
z_plonkish = [x_0,x_1,x_2,x_3,x_4,x_5,0,1,-1,2,]
z_ccs = [1,x_0,x_1,x_2,x_3,x_4,x_5]
## Checks performed by this Plonk/CCS instance:
# - binary check for x0, x1
# - 2*x2 + 2*x3 == x4
# CCS parameters
q=5
d=3
S = [[3,0,1], [4,0], [5,1], [6,2], [7]]
c = [1, 1, 1, 1, 1]
t = 8
n = 6
l = 0
m=4
# Initialize the result matrixes
matrixes = [[[0 for _ in range(t-1)] for _ in range(m)] for _ in range(t)]
# Iterate over i and j
for i in range(m):
for j in range(t):
k_j = T[i][j]
if k_j >= n:
matrixes[j][i][n - l - 1] = z_plonkish[k_j - n]
elif k_j >= n - l - 1:
matrixes[j][i][k_j + 1] = 1
else:
matrixes[j][i][k_j] = 1
Let's run the first iteration of this step explaining it here. Then the rest can be inferred.
Here, we select the row
Note that
So following the algorithm above, sice
Then we can set the coeff of
When
Then we set
When
Then we set
When
Then we set
When
Then we set
When
Then we set
When
Then we set
When
Then we set
Up to this point, we should have the following matrixes:
Note that in the paper is mentioned that any coefficient of the matrixes contained in
After the 4th iteration (So
Now that we have obtained 1
fixed instance for which we have padded our matrixes to our vector
Leading on
In the paper, we read:
For
, set to the coefficient of the th monomial of .
This is easy to see. If we represent the multivariate polynomial
where the monomials that form it are:
We can easily see that we don't have any constants/coefficients multiplying any of the monomials.
Hence, we conclude that they're all
This has a really easy translation if we actually name things.
Let's name each variable of the
In this case, if we recall, the plonkish instance was:
We will name/index all the columns in the
Naming therefore ->
Then we read in the paper:
For
, set if the th monomial contains a variable , where , add to the multiset with multiplicity equal to the degree of the variable.
Note that if we represent the multivariate polynomial
@arnaucube provided a modified version of the above code in which we can verify the instance used for the example on it.
# Plonk-CCS (https://eprint.iacr.org/2023/552) Sage prototype
# utils
def matrix_vector_product(M, v):
n = M.nrows()
r = [F(0)] * n
for i in range(0, n):
for j in range(0, M.ncols()):
r[i] += M[i][j] * v[j]
return r
def hadamard_product(a, b):
n = len(a)
r = [None] * n
for i in range(0, n):
r[i] = a[i] * b[i]
return r
def vec_add(a, b):
n = len(a)
r = [None] * n
for i in range(0, n):
r[i] = a[i] + b[i]
return r
def vec_elem_mul(a, s):
r = [None] * len(a)
for i in range(0, len(a)):
r[i] = a[i] * s
return r
# end of utils
# can use any finite field, using a small one for the example
F = GF(101)
# The following CCS instance values have been provided by Carlos
# (https://github.com/CPerezz) and Edu (https://github.com/ed255),
# and this sage script was made to check the CCS relation.
## Checks performed by this Plonk/CCS instance:
# - binary check for x0, x1
# - 2*x2 + 2*x3 == x4
M0 = matrix([
[F(0), 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1],
])
M1 = matrix([
[F(0), 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 1],
])
M2 = matrix([
[F(0), 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1],
])
M3 = matrix([
[F(1), 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
])
M4 = matrix([
[F(0), 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
])
M5 = matrix([
[F(0), 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
])
M6 = matrix([
[F(-1), 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
])
M7 = matrix([
[F(0), 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
])
z = [F(1), 0, 1, 2, 3, 10, 42]
print("z:", z)
assert len(z) == M0.ncols()
# CCS parameters
n = M0.ncols() # == len(z)
m = M0.nrows()
t=8
q=5
d=3
S = [[3,0,1], [4,0], [5,1], [6,2], [7]]
c = [1, 1, 1, 1, 1]
M = [M0,M1,M2,M3,M4,M5,M6,M7]
print("CCS values:")
print("n: %s, m: %s, t: %s, q: %s, d: %s" % (n, m, t, q, d))
print("M:", M)
print("z:", z)
print("S:", S)
print("c:", c)
# check CCS relation (this is agnostic to Plonk, for any CCS instance)
r = [F(0)] * m
for i in range(0, q):
hadamard_output = [F(1)]*m
for j in S[i]:
hadamard_output = hadamard_product(hadamard_output,
matrix_vector_product(M[j], z))
r = vec_add(r, vec_elem_mul(hadamard_output, c[i]))
print("\nCCS relation check (∑ cᵢ ⋅ ◯ Mⱼ z == 0):", r == [0]*m)
assert r == [0]*m
The most important thing for the code above is if we use the example with symbols rather than GF
elements. If we do this symbolic analisis, we get the following r
relation as a result of applying all the Haddamard products.
r = [x0^2 - x0, x1^2 - x1, 2*x2 + 2*x3 - x4, 0]
If we see the following r
expresion that results from each of it's elements represent exactly the constraints our plonkish instance had.
If you recall, our plonkish instance was defined as:
So as you see, each row of Plonkish has translated into an element of the vector r
which represents the symbolic result of
x^2 - x
Which is the boolean constraint!.2*x2 + 2*x3 - x4
is doublings of two variables and equaling it to another one as a constraint.r
equals 0.SuperSpartan translates to simply to the classical Spartan protocol but using CCS as arithmetization system.
To check the correctness of
What this is saying, is that if
This is esentially saving to the verifier the costs of computing the right hand side of the first equation of this section to actually evaluate
So once we are here, we can see that the problem can actually be translated to compute
Note that here
In Spartan we use a trick to actually move from N sumchecks to 2 using fiat shamir. But here we have
In this section we get a clear description of the protocol which I think is worth rephrasing here:
The paper also mentions the existance of an extension over SuperSpartan called SuperSpartan+
and an extension over CCS called CCS+
.
The description for this is very brief. Indeed it only mentions that in addition to the terms we already know a CCS
instance has, a CCS+
instance also contains:
Then, it's mentioned that in addition to all the previous requirements for a
In order for this to work, the Verifier needs to have oracle access to the polynomials
In order to support lookups, SuperSpartan+ encodes all the lookup operations
Then the prover sets the polynomial
As done in the original protocol, using the sum-check invocation the problem is reduced to check that the claimed relationship