This document assumes constrained elliptic curve is in SW form with a = 0
We require lots of sequential double and adding operations in ecc multiplication algorithms. Fortunately we have small but cool optimization trick to calculate P_2 = 2 * P_0 + P_1
. Rather than one doubling and one addition we combine two formulas and find a simpler P_2 = P_0 + P_1 + P_0
formula. Below we can calculate the result skipping calculation of y
coordinate of intermediate result.
lambda_0 = (y_1 - y_0) / (x_1 - x_0)
x_2 = lambda_0 * lambda_0 - x_0 - x_1
lambda_1 = lambda_0 + 2 * y_0 / (x_2 - x_0)
x_res = lambda_1 * lambda_1 - x_0 - x_2
y_res = lambda_1 * (x_res - x_0) - y_0
Random accumulation initializer is a point that should allow us to use incomplete formulas. In naive way in doubling we would need such constaints:
def double(P_0):
cond_is_inf = is_inf(P_0)
canditate_res = constrain_doubling(P_0)
return select(cond_is_inf, candidate_res, infinity)
So can we make sure that input point P_0
is not infinity without asserting each time while we constrain doubling?
And in addition it looks like this
def add(P_0, P_1):
cond_is_inf_0 = is_inf(P_0)
cond_is_inf_1 = is_inf(P_1)
cond_is_equal = is_equal(P_0, P_1)
canditate_res_add = constain_addition(P_0, P_1)
canditate_res_dou = constrain_doubling(P_0)
T = select(cond_is_equal, canditate_res_dou, canditate_res_add)
T = select(cond_is_inf_0, P_1, T)
T = select(cond_is_inf_1, P_0, T)
return T
In the intermediate step just before selection we have 5 candidate results! Also we had to apply doubling to stay safe while P_0 == P_1
.
c_0 | c_1 | c_eq | R |
---|---|---|---|
1 | 1 | guaranteed to be 1 | infinity |
0 | 1 | guaranteed to be 0 | P_0 |
1 | 0 | guaranteed to be 0 | P_1 |
0 | 0 | 1 | 2P_0 == 2P_1 |
0 | 0 | 0 | P_0 + P_1 |
Below conditions are ideally what we would like to have that operands satisfies to make formulas much shorter.
P_0 != 0
and P_1 != 0
to skip infinity checksP_0 != P_1
to just constrain additionP_0 != -P_1
to avoid result goes to infinity.And also keep in mind that we are not using an embedded curve ie. base field of the subject elliptic curve is not equal to the scalar field where we constrain equations. Therefore infinity and equality checks becomes more expensive than we would like.
We can just make sure that any public inputs are not point of infinity by simply checking if point is on curve. However this won't guarantee P_0 == P_1
to escape branching in addition since an earlier addition with P_0 = -P_1
operands would produce an infinity inside of the circuit.
Let's denote multiplication like R = P * e
where e
is a scalar and can be decomposed as [b_0, b_1, ...]
def mul(P_0, e):
bits = decompose(e)
table = [infinity, P_0]
Acc = select(bits[0], table)
for b in bits[1:]:
Acc = double(Acc)
to_add = select(b, table)
Acc = add(Acc, to_add)
return Acc
In the naive multiplication above in the table we have point of infinity as first entry. Constraints must follow complete formulas even if input point P_0
is guaranteed not to be point of infinity.
So we will use an auxiliary point AuxInit
which is fed with public inputs in order to avoid using complete formulas. Then selection table is now [AuxInit, AuxInit + P_0]
rather than [infinity, P_0]
AuxInit
AuxFin = AuxInit * ((1 << n) -1)
def mul(P_0, e):
bits = decompose(e)
table = [AuxInit, P_0 + AuxInit]
Acc = select(bits[0], table)
for b in bits[1:]:
Acc = double_incomplete(Acc)
to_add = select(b, table[0], table[1])
Acc = add_incomplete(Acc, to_add)
# or more in optimal way
# Acc = ladder_incomplete(Acc, select(b, table[0], table[1]))
Acc = Acc - AuxFin
return Acc
So we not only achieved non infinity point initialization but also guaranteed operands in addition will not be equal! It means we don't have to branch for infinity points in the circuit.
This claim must hold only if prover doesn't have an access to AuxInit
point before creating the proof.
So we can just precompute (or preconstrain) a wider table to reduce number of iterations in the double-and-add algorithm. While this step introduce more selection steps it will reduce number of additions.
For example wherewindow_size = 2
a scalar e
is decomposed as:
bits = [b_0, b_1, ...]
and we window those bits as
windows = [[b_0, b_1], [b_2,b_3], ...]
So selection table will be
table = [0, P_0, 2*P_0, 3*P_0]
Or with auxiliary point
table = [Aux, P_0 + Aux, 2*P_0 + Aux, 3*P_0 + Aux]
window_size
times.1/window_size
times.2^window_size
. So a sweet window size can be found.Sliding window technique is cheaper because selection operation is much cheaper than addition operation (even if incomplete addition).
Multi multiplication mostly covers linear combination of points and has some application area for example can be used in recursion circuits or polynomial commitment inside of a circuit.
So we can generalize simple double-and-add algorithm to share the same doubling step across point scalar pairs. We will also use sliding window and auxiliary initial value technique.
pairs = [(P_0, e_0), (P_1, e_1), (P_2, e_2)]
Scalar values decomposed as:
e_0 = [b_0, b_1, ...]
e_1 = [b_0, b_1, ...]
e_2 = [...]
Let's window scalars:
w_0 = [[b_0, b_1], [b_2,b_3], ...]
w_1 = [[b_0, b_1], [b_2,b_3], ...]
w_2 = [...]
AuxGen
is random point where DL is unknown that generates aux values for each pair. We should calculate an aux per pair and we have to make it linear independent to use incomplete formulas ie. to avoid a=b
in add(a,b)
operation. Each pair could have their own independent aux points however we expect this aux point from public input and we also don't want to bloat public input size. So we can still a generator point to make tables independent.
A_0 = AuxGen * 2^0
A_1 = AuxGen * 2^1
A_2 = AuxGen * 2^2
Then we will be able to construct tables as:
table_0 = [A_0, A_0 + P_0, A_0 + 2 * P_0, A_0 + 3 * P_0]
table_1 = [A_1, A_1 + P_1, A_1 + 2 * P_1, A_1 + 3 * P_1]
table_2 = [...]