Try   HackMD

MACI v1.0: pairwise coordination subsidy

Introduction

Quadratic funding is a protocol for efficiently allocating funds to public goods. Suppose there are total

m projects (i.e.
{p1,,pm}
) and
n
contributors (i.e.
{u1,,un}
). Each contributor
ui
can allocate his/her money into different projects. We can record his/her opinion (or votes) into an array
(vi1,,vim)
. The actual contribution of
ui
is the square of the votes
Ci=(vi12,,vim2)
. This is why we use the name quadratic funding.

Given a project

l{1,,m}, the total allocation to this project is the sum of contributions from contributors with subsidy allocated by the protocol's public funding pool. The basic form of calculating the subsidy is illustrated by the following picture:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

where the green squares are the contributions from different contributors to project

pl. Each of the yellow rectangular is the subsidy allocated by the protocol according to the contributions of each contributor pair
(ui,uj)
to this project. Thus total area of the big square is the total funding to this project:
Fl=(invil)2

The subsidy allocated by the public funding pool (assuming we have enough public fund) is given by
Sl=(invil)2invil2=ijvilvjl

There is one issue from the basic quadratic funding, if two contributors collude with each other, they can extract most of the public funding pool if they have enough money. In this post, Vitalik introduced this kind of collusion and also proposed a protocol to penalize this behavior.

In order to penalize the potential collusion from contributors, we generalize the subsidy formula:

Sl=ijkijvilvjl
Notice that when
kij=1
, it's the basic clr formula and when
kij=0
, there is no subsidy allocated to the project. So the general idea is that the more correlation between
(ui,uj)
, the smaller
kij
, hence the smaller subsidy allocated to this project. Vitalik proposed the following
kij
coefficient:
kij=MM+(l=1mvilvjl)α

Here
M
and
α
are protocol related constants. In this case, there is an upper bound that the contributors can extract from the public funding pool.

In the general form, the subsidy allocated by public pool:

Sp=ijkijvipvjp=(ijMM+(vilvjl)α(vipvjp)α)1α
α=2
is defined in strange kind QF. In our application, we will choose
α=1
which is defined in the normal pairwise bound QF. i.e.

Sp=ijMM+l=1mvilvjl(vipvjp)

One might argue what if the contributor chooses different identities in different project? The basic assumption to the quadratic funding (also quadratic voting) is that we need depend on the identity system (e.g. KYC) to identify user's identity.

Pseudocode

// input: V = [u1,u2,...,un]. ui = [vi1,vi2,...,vim] // ui is the votes for contributor i // vij is coordinator i's vote on option j. // Step 1: calculate kij coefficients kij = [] for (let i=0;i<n; i++){ for (let j=i+1;j<n;j++){ let sum = 0; for (let l=0;l<m;l++){ sum += V[i][l]*V[j][l]; } kij.push(M/(M+math.power(sum,alpha))); } } // Step 2: calculate subsidy S = [] for (let l=0;l<m;l++){ let idx = 0; let sum = 0; for (let i=0;i<n;i++){ for (let j=i+1;j<n;j++){ sum += 2*kij[idx]*V[i][l]*V[j][l]; // by symmetry of kij } } idx += 1; S.push(sum); }

In Step

1, number of operations is
O(n2m)
, storage is
O(n2)
. In Step
2
, number of operations is
O(n2m)
, storage is
O(m)
. Thus, the total number of operations is
O(n2m)
, storage is
O(n2+m)
.

Represent float number

Notice that

0kij1. The first thing we need to do is to represent float number. In circom, everything is in finite field
Fq
, where
q=21888242871839275222246405745257275088548364400416034343698204186575808495617

The field element can be represented by
253
bits. One solution is to represent last
N
bits as decimal part and the first
253N
bits as integer part. Here are the basic rules:

  1. aa10N
    :
    15001.5
  2. division
    a/ba2N/b
    :
    1/21103/2=500(0.5)
  3. multiplication
    abab10N
    :
    (0.25)20050002005000103=1000(1)
  4. addition and substraction
    a+b,ab
    same as before

Here is the sage code sample:

p = 21888242871839275222246405745257275088548364400416034343698204186575808495617 F = GF(p) class FF: N = 4 # how many bits used for float part; N not too large to avoid overflow def __init__(self, x, adjust = False): # assume x is in F if adjust: self.v = x*10**FF.N else: self.v = x def mul(self, y): m = mod(self.v * y.v, 10**FF.N) res = (self.v * y.v - F(m))/10**FF.N #res = (self.v * y.v) >> FF.N return FF(res) def add(self, y): res = self.v + y.v return FF(res) def div(self, y): m = mod(self.v * 10**FF.N,y.v) res = ((self.v * 10**FF.N) - F(m))/y.v return FF(res) def sub(self, y): res = self.v - y.v return FF(res) def __str__(self): return self.v.__str__() def real(self): # print the real number representation pass

Test cases:

a=FF(F(1),true)
b=FF(F(2),true)
c=FF(F(3),true)
d=FF(F(4),true)
e=FF(F(6),true)

x=a.div(b)
y=a.div(c)
z=a.div(d)
w=a.div(e)

print(x) #1/2 => 5000 
print(y) #1/3 => 3333
print(z) #1/4 => 2500
print(w) #/1/6 => 1666

This is the integer division instead of finite field division. Thus, we need write a circuit that can calculate integer division with float representation.

pragma circom 2.0.0; include "./node_modules/circomlib/circuits/bitify.circom"; include "./node_modules/circomlib/circuits/comparators.circom"; include "./node_modules/circomlib/circuits/mux1.circom"; template IntegerDivision(n) { // require max(a, b) < 2**n signal input a; signal input b; signal output c; assert (n < 253); assert (a < 2**n); assert (b < 2**n); var r = a; var d = b * 2**n; component b2n = Bits2Num(n); component lt[n]; component mux[n]; component mux1[n]; for (var i = n - 1; i >= 0; i--) { lt[i] = LessThan(2*n); mux[i] = Mux1(); mux1[i] = Mux1(); } for (var i = n-1; i >= 0; i--) { lt[i].in[0] <== 2 * r; lt[i].in[1] <== d; mux[i].s <== lt[i].out; mux[i].c[0] <== 1; mux[i].c[1] <== 0; mux1[i].s <== lt[i].out; mux1[i].c[0] <== 2 * r - d; mux1[i].c[1] <== 2 * r; b2n.in[i] <== mux[i].out; r = mux1[i].out; } c <== b2n.out; }

Coefficient and Subsidy Calculation

From above section, we can see that due to limitation of circom 2.0 compiler restriction. It's difficult to do the coefficient calculation by batch. And if we calculate all together, the circuit will be too big to compile/generate proof. Instead of saving the calculation of coefficients as upper triangle

{kij}0i<j<N, we can calculate the coefficients redundantly, i.e.
{kij}0i<N,0i<N
. With this approach, we can divide the matrix evenly, where each batch is of size B*B, B < N. In this case, there are total ceil(N/B)*ceil(N/B) batches. We can index each batch by its starting index (rowStartIndex, colStartIndex). For each of the batch, we have two inputs

rowVotes=(votes[rowStartIndex],...votes[rowStartIndex+B-1])  
colVotes=(votes[colStartIndex],...,votes[colStartIndex+B-1])

Choose the batchSize B=treeArity**subTreeDepth, then it is easy to prove the votes are part of vote trees. And since the order of the batch index is regular, we don't have to do order check anymore. For each batch, we add its contribution to previous the subsidy result. After we process all the batches, the last subsidy result is the final one.

For example, suppose we have

4 users
{u1,u2,u3,u4}
in
2
batches (i.e.
B1={u1,u2}
,
B2={u3,u4}
), we need calculate
6
correlation coefficients:
{u1,u2,u3,u4}{c12,c13,c14,c23,c24,c34}

We do it by
4
pair of batches:
{(B1,B1),(B1,B2),(B2,B1),(B2,B2)}

Avoid overflow in subsidy calculation

When we use finite field

FN to emulate real number operations, we need avoid overflow. Especially, we need to make sure that the denominator of
kij
and the final sum are not greater than modules number
N
. For simplicity, suppose we have an upper bound for any given voter's total vote:
l=1mvilV
. The sum in denomiator has maximum value when two voters vote for the same project:
l=1mvilvjlV2

However, the last sum is not easy to estimate the bound, as the larger correlation will has smallar coefficient. A rough estimate would be:
ijkijvipvjpMM+V2/mV2n(n1)n2mM

  • V2+M<N
  • n2mM<N

We can pick

2252<N as a safe bound (because the circomlib LessThan require
n<2252
). In practice, we have
n510,m54
. Thus, for example, if
M<2196,V<2125
, above two conditions are met.

reference

clr paper
pairwise bound QF
strange kind QF

tags: public maci