# Anàlisi tècnica en profunditat: vot anònim amb zk-SNARKs

# **Què entenem per vot anònim**

Abans d'entrar en les especificacions tècniques, val la pena explicar com definim el vot anònim i per què vam arribar a aquest disseny. A l'article d'anunci del producte presentem dos tipus d'anonimat: l'ofuscació de la papereta i l'anonimat del votant.

L'ofuscació de la papereta és l'anonimat que s'obté amagant el contingut de cada vot, mentre que l'anonimat del votant trenca el vincle entre cada votant i el seu vot, sense amagar els vots en si. La nostra implementació del vot anònim fa servir l'anonimat del votant. Per què?

En primer lloc, l'ofuscació de la papereta comporta contrapartides importants. Perquè un procés de votació sigui verificable d'extrem a extrem, els votants han de poder seguir i examinar el contingut de les paperetes que han emès. Si garantim l'anonimat ofuscant les paperetes, perdem la verificabilitat: és impossible verificar que el teu vot s'ha comptat i auditar la integritat de la votació si el contingut de cada vot està amagat.

Un altre inconvenient de l'ofuscació de la papereta és que és difícil d'implementar en un sistema de votació digital. Una de les tècniques d'ofuscació és el xifratge homomòrfic, que s'ha [proposat](https://www.itm-conferences.org/articles/itmconf/pdf/2020/02/itmconf_icacc2020_03023.pdf) per aportar anonimat criptogràfic als sistemes de votació tradicionals. El xifratge homomòrfic permet fer càlculs sobre valors xifrats, de manera que es pot calcular el recompte de vots sense examinar el contingut de cap papereta. Tot i ser sòlid en teoria, el xifratge homomòrfic té molts inconvenients que el fan poc desitjable com a base d'una tecnologia de votació.

El més destacable és que aquest mètode només permet càlculs bàsics sobre les paperetes xifrades, cosa que fa més difícil d'[implementar](https://blog.aragon.org/vocdoni-ballot-protocol/) esquemes com el [vot](https://www.wikiwand.com/en/Instant-runoff_voting) [quadràtic](https://en.wikipedia.org/wiki/Quadratic_voting?oldformat=true) o el [preferencial](https://en.wikipedia.org/wiki/Instant-runoff_voting?oldformat=true). A més, tot i que el càlcul dels resultats està garantit amb el xifratge homomòrfic, la verificació d'extrem a extrem de la papereta d'un votant no ho està. Aquest esquema no ofereix cap prova criptogràfica que garanteixi que la papereta d'un votant s'ha inclòs i representat correctament al conjunt de resultats. També estimem que la validació de la prova homomòrfica d'un procés de votació exigiria recursos computacionals significatius, que a més creixen amb la mida del procés. Això podria anul·lar els avantatges d'un sistema universalment verificable, perquè la verificació només es podria fer en màquines potents i cares, en un termini prohibitivament llarg.

L'anonimat del votant té diversos avantatges respecte del xifratge homomòrfic. Primer, permet un grau molt més alt de verificabilitat d'extrem a extrem. Tot i que el vincle entre un votant i la seva papereta ha de ser opac per a qualsevol observador _extern_, els votants sí que poden identificar la seva papereta. Com que les paperetes són visibles, qualsevol usuari pot seguir el seu vot des del moment que l'emet fins a la seva inclusió en els resultats. A més, els tercers poden comprovar que cada papereta pertany a un votant vàlid i que el seu contingut s'ha comptat correctament.

L'anonimat del votant també tendeix a permetre dissenys molt més eficients computacionalment. Els sistemes amb anonimat del votant apliquen l'ofuscació un sol cop, sobre la prova de la papereta de cada votant, en lloc de fer-ho sobre tot el conjunt de resultats cada vegada que s'emet una nova papereta. No cal cap pas computacional important per fer els resultats àmpliament accessibles. Fins i tot es poden publicar resultats _durant_ el procés de votació sense comprometre l'anonimat (sempre que s'hagin mitigat els atacs de correlació temporal).

Per tot això, hem decidit incorporar l'anonimat del votant al nostre mecanisme de prova de cens.

# **Arbre de Merkle del cens**

El punt de partida del mecanisme de cens de votants és un [_arbre de Merkle de cens_](https://docs.vocdoni.io/architecture/census/off-chain-tree.html). Es tracta d'una estructura de claus públiques amb hash, cadascuna de les quals representa un votant elegible del cens. L'ús d'un arbre de Merkle permet que qualsevol votant vàlid demostri que té una `censusKey` que pertany al cens, sense conèixer les claus de cap altre votant. El votant envia aquesta prova com a part de la seva papereta. A aquest _sobre de vot_ també s'hi adjunta un `nullifier`: una dada derivada de la `censusKey` de l'usuari i de l'`electionID` que correspon de manera única al seu vot.

Si es fes servir només aquesta prova, l'organitzador d'un procés podria correlacionar cada sobre de vot, a través del seu `nullifier`, amb la `censusKey` d'un votant, i desanonimitzar la votació. Per evitar-ho, Vocdoni fa servir _zk-SNARKs_ per garantir l'anonimat del vot.

### **zk-SNARKs per al vot anònim**

[zk-SNARK](https://en.wikipedia.org/wiki/Non-interactive_zero-knowledge_proof?oldformat=true) són les sigles de &#x7A;_&#x65;ro-knowledge_ _Succinct Non-interactive ARgument of Knowledge_. Un zk-SNARK permet que un usuari demostri a un tercer que posseeix una determinada informació, sense revelar la informació en si.

En el nostre cas, els zk-SNARKs permeten als votants demostrar que pertanyen al cens sense revelar la seva `secretKey`. En concret, això es fa amb un zk-Circuit, un circuit de programari dissenyat per generar una prova de coneixement zero (ZKP).

### **zk-Circuit**

Hem dissenyat un circuit que permet als usuaris generar una ZKP de la seva pertinença al cens sense revelar la seva `secretKey`. El circuit rep entrades privades i públiques, i les dades que podrien revelar la identitat del votant es mantenen privades. Les entrades públiques s'envien dins del sobre de vot perquè els validadors les puguin contrastar amb la prova i assegurar-se que l'usuari no ha votat dues vegades.

El circuit es pot fer servir per a qualsevol procés amb un cens de mida similar, i només cal generar-lo un cop per a cada mida. Més concretament, un circuit es pot generar i reutilitzar per a qualsevol arbre de Merkle de cens amb la mateixa alçada d'arbre. Com que fem servir un arbre binari, això vol dir que cal un circuit per a cada valor `n` tal que la mida del cens objectiu estigui dins del rang de `2^n` (p. ex. 128, 256, ..., 8192, 16384, etc.).

La generació del circuit depèn d'una [cerimònia de configuració de confiança](https://medium.com/qed-it/diving-into-the-snarks-setup-phase-b7660242a0d7) (_trusted setup_). És el procés que genera les claus de provador i de verificador del circuit, i es diu «de confiança» perquè, si una sola part controlés tota la generació d'aquestes claus, podria generar proves falses. Per això cal una «cerimònia» descentralitzada que asseguri la fiabilitat del circuit. En una cerimònia així, diverses parts amb interessos contraposats i en ubicacions diferents fan cadascuna un pas de la generació de claus. Si només una d'aquestes parts manté la integritat, serà impossible generar una prova falsa.

La `franchise proof` es genera executant el circuit zk-SNARK.

- **Entrades privades:** `index`, `secretKey`, `census Merkle-proof`
- **Entrades públiques:** `census Merkle-root`, `nullifier`, `election ID`, `vote`
- **Sortida:** `franchise proof`

![](/blog/images/2026/01/zksnarks_anonymous_1.webp)

Sense revelar la `secretKey` ni la _prova de Merkle del cens_, aquest circuit és capaç de demostrar que:

1. El votant és el propietari de la `secretKey` corresponent a una determinada `zkCensusKey`.
2. La `zkCensusKey` del votant està inclosa a l'arbre de Merkle del cens.
3. El `nullifier` que aporta el votant correspon de manera única a la seva `secretKey` i a l'`election ID` d'un procés de votació concret.

Tot i que el càlcul és intensiu en CPU i memòria, les ZKP es poden generar des del client de l'usuari, en maquinari modest. Al protocol de [Vocdoni](https://vocdoni.app), la prova la validen els nodes de la [Vochain](https://docs.vocdoni.io/architecture/services/vochain.html), els miners i qualsevol tercer que monitori el procés.

El circuit està minimitzat i optimitzat fins als seus requisits mínims per fer-lo accessible a qualsevol tipus de maquinari de client. Aquesta és la raó per la qual no fem servir verificació de signatura sinó un enfocament basat en secretKey.

### **Arbres de Merkle i de cens**

L'arbre de Merkle que es fa servir per construir el _cens anònim_ ha de ser una implementació compatible amb zk-SNARKs. Com que actualment fem servir el compilador [Circom](https://github.com/iden3/circom) per als circuits zk-SNARK, necessitem un arbre compatible amb la implementació d'arbre de Merkle de [circomlib](https://github.com/iden3/circomlib/tree/master/circuits/smt). [Aquí pots trobar](https://docs.iden3.io/publications/pdfs/Merkle-Tree.pdf) una especificació d'aquest arbre de Merkle.

La [Vochain](https://docs.vocdoni.io/architecture/services/vochain.html) fa servir l'arbre de Merkle [arbo](https://github.com/vocdoni/arbo), una implementació en Go compatible amb el disseny de Circom. L'arbre de Merkle fa servir el hash Poseidon, una funció de hash «amigable amb SNARKs» que després es pot provar dins d'un circuit sense requerir massa restriccions. El diagrama següent és una representació visual de l'estructura de dades de les fulles de l'arbre de Merkle que es fa servir en l'esquema de la zk-census-proof.

![](/blog/images/2026/01/zksnarks_anonymous_2.webp)

El valor `index` el determina el constructor de l'_arbre del cens_, que té un valor `index` per a cada arbre de cens. Aquest valor s'incrementa amb l'addició de cada nova _fulla_. Les _fulles_ s'estructuren així per fer servir els arbres de Merkle de manera més eficient, fent cabre les claus de més usuaris dins d'un arbre més petit i reduint així la mida del zk-Circuit. Això és perquè el `value` de la `key` de qualsevol fulla determina la posició d'aquesta fulla a l'arbre.

Si la clau de la fulla la determinés la `zkCensusKey`, en lloc d'un `index` incremental, cada nova fulla tindria una probabilitat significativa de col·lisió abans d'omplir totes les posicions de fulla disponibles per a una alçada donada. Els arbres serien, per tant, menys equilibrats i requeririen circuits més grans per a la mateixa mida de cens, per un ús ineficient de l'espai de l'arbre. Amb l'enfocament de l'índex incremental, en canvi, es poden omplir totes les posicions de fulla sense ni una sola col·lisió. Això produeix circuits molt més petits per al mateix nombre d'usuaris.

### **Generació de les zk-Inputs**

```json
{
	"censusRoot": "51642541620950251760298704744678482162425252475654827255045491135352807540162",
	"censusSiblings": ["0","0","0","0"],
	"index": "30",
	"secretKey": "6190793965647866647574058687473278714480561351424348391693421151024369116465",
	"voteValue": ["100964581237483263846637432502620436451", "278307331411790712608582894981321409946"],
	"electionId": "10",
	"nullifier": "1938187656076799017313903315498318464349291455761501098436114043715056719301",
}
```

Origen de cada paràmetre de zk-Input:

Tots els paràmetres són `string` o `[]string` que representen `bigInt` o `[]bigInt`.

- `censusRoot`: el calcula l'_autoritat del cens_ a partir de l'_arbre del cens._
- `censusSiblings`: els calcula l'autoritat del cens; és la prova de Merkle
- l'usuari obté els _siblings_ de la Vochain a través del gateway.
- la longitud de `censusSiblings` dependrà del zk-Circuit:
- El disseny de l'arbre de Merkle que es fa servir a circomlib provoca que es retornin longituds diferents dels siblings en generar una prova de Merkle.
- La profunditat de l'arbre, definida des de l'arrel fins a les fulles, dependrà de cada fulla i dels seus veïns.
- Pots trobar més detalls a l'[especificació de l'arbre de Merkle](https://docs.iden3.io/publications/pdfs/Merkle-Tree.pdf).
- Per poder introduir aquests siblings al circuit, el paràmetre `nLevels` del circuit és fix, així que `siblings.length` també ha de ser fix.
- La `siblings.length` dependrà del zk-Circuit que es faci servir, concretament del paràmetre `nLevels` del circuit
- La lògica que cal implementar al costat de l'usuari es pot trobar [aquí (go), línies 67-70](https://github.com/vocdoni/zk-franchise-proof-circuit/blob/feature/go-code-inputs-generation/test/go-inputs-generator/census_test.go#L67), i [aquí (js), línia 23](https://github.com/vocdoni/zk-franchise-proof-circuit/blob/feature/go-code-inputs-generation/src/franchise.js#L33): `while (siblings.length < this.levels) siblings.push(BigInt(0));`
- `index`: el determina la Vochain en afegir la `zkCensusKey` de l'usuari a l'arbre del cens
- `secretKey`: la genera l'usuari
- `voteValue`: valor amb hash del vot de l'usuari, format per dos enters grans.
- El vot en cru de l'usuari és un array de valors de longitud variable, i els seus valors no cal comprovar-los al circuit. A més, els valors poden estar xifrats.
- Com que els valors codificats del vot poden no cabre en un nombre constant d'entrades del circuit, calculem un resum del vot en cru de l'usuari amb una funció de hash compatible amb l'EVM: `sha256(vote_bytes)`. La sortida del hash `sha256` és lleugerament més gran que el camp que es fa servir en SNARKS, així que dividim la sortida del hash (32 bytes) en 2 arrays de 16 bytes, els prenem com a enters (en little-endian) i els fem servir com a entrades del circuit.
- Es fa servir el hash `sha256` perquè, si en el futur calgués, es pot [verificar dins](https://github.com/iden3/circomlib/blob/master/circuits/sha256/sha256.circom) del circuit. Aquest ús té dues característiques a tenir en compte: `sha256` és el doble de car que `keccak256` en termes de gas a l'EVM, però està implementat en circom, de manera que es pot comprovar dins d'un circuit; comprovar el `sha256` dins d'un circuit de circom és car en termes de nombre de restriccions (en la versió actual d'aquesta especificació, això no es comprova dins del circuit).

```go
h := sha256.Sum256(voteBytes) // voteBytes can be the votes array converted to bytes, or the encrypted votes
b1 := new(big.Int).SetBytes(swapEndianness(h[:16])) // swap endianness, as golang big int package works in big-endian, and we use little-endian
b2 := new(big.Int).SetBytes(swapEndianness(h[16:]))
```

I l'entrada json del `voteValue` per al circuit seria: `"voteValue": [b1, b2]`

- `electionID`: l'ID de l'elecció en què participa l'_usuari_
- `nullifier`: el calcula l'_usuari_: `nullifier = poseidon.Hash(sk, electionID)`

# **Vot anònim per a DAOs**

Per respondre la pregunta «com es pot fer servir aquesta tecnologia per a les votacions de DAOs a Ethereum», cal afegir una mica de context:

- Actualment, la criptografia interna d'Ethereum, les seves estructures de dades i el seu arbre de merkle no són compatibles amb zk-SNARKs, així que les opcions són limitades (de moment).
- Vocdoni suporta actualment el [vot amb proves d'emmagatzematge d'Ethereum](https://www.notion.so/Introducing-Vocdoni-Bridge-voice-cf7e73d38c4a45788358e9a1497cdf19?pvs=21), que ara mateix és la manera més segura i sense confiança d'executar votacions offchain sense gas (implementat al frontend de [Aragon Voice](https://blog.aragon.org/introducing-vocdoni-anonymous-voting/))
- Tot i que Vocdoni encara no ofereix execució vinculant offchain completa (votar dins d'Ethereum requereix un enfocament optimista), hem fet [proves de concepte en aquest àmbit](https://blog.aragon.org/binding-execution-on-ethereum-with-zk-rollups/) i seguim investigant per assolir totes les propietats (aviat arribaran nous dissenys i proves de concepte).

La manera com Vocdoni pot suportar actualment el vot anònim en DAOs és seguint aquests passos:

1. Es crea una nova elecció a la blockchain de Vocdoni amb un censusRoot igual a l'arrel d'estat d'Ethereum i l'adreça del contracte ERC20.
2. Els usuaris obtenen de Web3 una prova d'emmagatzematge d'Ethereum que demostra que tenen tokens per a un contracte i una arrel d'estat.
3. Els usuaris generen una nova `secretKey` temporal i envien una transacció a la blockchain de Vocdoni per demostrar que són votants elegibles (fent servir la prova d'emmagatzematge). Aquesta transacció inclou la `secretKey` amb hash, que s'afegeix a un `rolling Census` que la lògica de la blockchain de Vocdoni calcula internament. Aquest pas s'anomena **preregistre de claus**.
4. Un cop acabat el preregistre, els usuaris poden votar de manera anònima amb la seva `secretKey`.
