Recursive Arguments

2022-12-11

Recursive arguments are a technique for compressing what would be a linear sized proof into a logarithmic one. They first showed up in the context of electronic voting as a way to prove an honest permutation of ballots and have since been used to build more general succinct arguments.

Given a polynomial t(x)F_q[x] we’d like to argue the validity of a commitment to the evaluation of t at a given point. Polynomial evaluation can be expressed as an inner product of the form t(x)=i=0d1tixi=t,x where t=(t0,t1,t2,,td1)Fqd is a vector of coefficients and x=(1,x,x2,,xd1) is a vector of successive powers of x.

Public Parameters

Fix some cyclic group G where the discrete log problem is hard and generate the following public parameters.

# Polynomial degree
d = (1 << 10)

# Curve of prime order
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
E = EllipticCurve(GF(p), [0, 7])
q = E.order()

# Scalar field
K = GF(q)

# Group generators
g = [E.random_element() for _ in range(d)]
h = [E.random_element() for _ in range(d)]
u = E.random_element()

Pedersen Hash

The Pedersen hash of two vectors is the group element satisfying

P(t,x)=t,g+x,hG

where g and h are the fixed generator vectors defined above. This hash function is desirable because of its additive properties

P(t,x)+P(v,y)=P(t+v,x+y)

αP(t,x)=P(αt,αx)

We can extend this to commit to the inner product of the two vectors

com(t,x)=P(t,x)+t,xuG

Where u is the fixed group generator defined above.

# Inner product
ip = lambda t,x: sum([i*j for (i,j) in zip(t, x)])

# Pedersen hash
com = lambda t,x,g,h: ip(t, g) + ip(x, h) + ip(t, x)*u

# Sample coefficient vectors
t = vector(K, [K.random_element() for _ in range(d)])
x = vector(K, [K.random_element() for _ in range(d)])

# Derive commitment
c = com(t, x, g, h)

Recursive Argument

At this point, we’ve commit to the coefficients of two vectors along with their inner product using a single group element. The next step is to generate a proof of knowledge of this commitment’s pre-image to convince a verifier that C is of the right form.

# Left half of a vector
L = lambda v: v[:len(v)//2]

# Right half of a vector
R = lambda v: v[len(v)//2:]

Let v_L and v_R denote the left and right half of a vector v. At each step of the argument, the prover sends two intermediate commitments that account for vectors of half the original size.

CL=com(tR,xL,gL,hR)GCR=com(tL,xR,gR,hL)G

The verifier responds with a random challenge rF_q which is used to update the parameters for the next step

xxL+r1xRggL+r1gRttL+rtRhhL+rhRCrCL+C+r1CR

The trick here is that the last step lets the verifier indirectly compute the updated commitment

rCL+C+r1CR=com(t,x,g,h)

without ever using the vectors themselves. If we continue to halve at each step, we’ll eventually reach our base case where t and x are of length one. At this point, we send over the two scalars and conclude the proof.

Technical note: as is common with interactive proofs, we apply the Fiat-Shamir transform and replace the random challenge with a hash of the intermediate commitments to make the scheme non-interactive.

# Fiat-Shamir transform
def fst(l, r):
    from hashlib import blake2b
    # Integer to bytes
    i2b = lambda i: i.to_bytes((i.bit_length() + 7) >> 3, 'big')
    h = blake2b()
    h.update(b'fiat-shamir-transform')
    payload = b''.join([i2b(int(i)) for i in l[:] + r[:]])
    h.update(payload)
    return K(int(h.hexdigest(), base=16))

def prove(t, x, g, h):
    if len(t) == 1:
        return [t, x]
    else:
        # Decompose
        cL = com(R(t), L(x), L(g), R(h))
        cR = com(L(t), R(x), R(g), L(h))

        # Derive challenge
        r = fst(cL, cR)
        r_inv = r^-1

        # Update parameters
        x = vector(K, L(x)) + r_inv*vector(K, R(x))
        g = [i+j for (i,j) in zip(L(g), [r_inv*k for k in R(g)])]
        t = vector(K, L(t)) + r*vector(K, R(t))
        h = [i+j for (i,j) in zip(L(h), [r*k for k in R(h)])]

        return [cL, cR] + prove(t, x, g, h)

proof = prove(t, x, g, h)
assert(len(proof) == 2*log(d, 2) + 2)

The Verifier’s View

At this point, the verifier has the following view

  1. The original commitment: CG

  2. The set of intermediate commitments: CL(i),CR(i)0ilogd

  3. The set of challenge values: ri0ilogd

The verifier proceeds through the set of intermediate commitment pairs, deriving the challenge at each step while keeping track of the updated generator vectors (g,h) and commitment C. After reaching the base case, the verifier uses the vectors of length one (t,x) to check

C=?com(t,x,g,h)

If this relation holds, the verifier is convinced that (1) the intermediate values were generated honestly and (2) the original commitment is of the correct form.

def verify(c, g, h, proof):
    if len(proof) == 2:
        t, x = proof[0], proof[1]
        return c == com(t, x, g, h)
    else:
        (cL, cR) = proof[:2]

        # Derive challenge
        r = fst(cL, cR)
        r_inv = r^-1

        # Update parameters
        g = [i+j for (i,j) in zip(L(g), [r_inv*k for k in R(g)])]
        h = [i+j for (i,j) in zip(L(h), [r*k for k in R(h)])]
        c = (r*cL) + c + (r_inv*cR)

        return verify(c, g, h, proof[2:])

assert(verify(c, g, h, proof))

References

  1. https://eprint.iacr.org/2017/1066.pdf
  2. https://dankradfeist.de/ethereum/2021/07/27/inner-product-arguments.html