Saltar al contenido
Gorka Hernandez Villalon, desarrollador iOS y especialista en automatizacion con IAGorka Hernandez
Volver al blog
FadeChainBlockchainZero-KnowledgeSolidityEthereum

FadeChain: como construimos una votacion blockchain privada con zero-knowledge

FadeChain explicado: votacion privada en Ethereum Sepolia con zk-SNARKs, Merkle trees, relayer y voto ponderado por tiempo.

31 de mayo de 2026 9 min de lecturapor Gorka Hernandez Villalon

FadeChain fue el proyecto que construimos en una hackathon de blockchain de la asignatura de Criptografia y Seguridad de Ingenieria UPF. El reto era crear una propuesta innovadora sobre la red de pruebas Ethereum Sepolia, con una idea tecnica realista, un caso de uso coherente y una demo que pudiera enseñarse al jurado.

Nuestro equipo fue FadeChain, formado por Gorka Hernandez, Sara Lopez, Arnau Carbonell y Jordi Lleopart. El proyecto termino siendo uno de los ganadores de la hackathon.

La idea central era resolver una tension muy tipica en gobernanza digital: como permitir que una persona vote de forma privada, sin revelar su identidad, pero manteniendo resultados publicos, auditables e inmutables en blockchain.

El problema que queriamos resolver

En una votacion tradicional on-chain, todo queda publicado: direccion de wallet, transaccion, timestamp y opcion votada. Eso es perfecto para auditar, pero muy malo para el secreto de voto.

En el extremo contrario, una votacion privada fuera de blockchain puede ocultar identidades, pero entonces aparece otra pregunta: quien garantiza que el recuento no se ha manipulado?

FadeChain intenta unir las dos cosas:

  • Privacidad para el votante: nadie deberia poder saber que persona voto que opcion.
  • Verificabilidad publica: los votos aceptados, pesos y resultados quedan en contrato.
  • Prevencion de doble voto: una persona registrada solo puede votar una vez.
  • Reglas inmutables: fechas, numero de opciones y parametros criticos se fijan al desplegar.
  • Incentivo a votar pronto: el voto pierde peso conforme avanza la ventana de votacion.

Ese ultimo punto fue nuestro giro de producto. Queríamos reducir el efecto arrastre: si todo el mundo espera al final para ver hacia donde va la mayoria, la votacion se vuelve menos deliberativa. En FadeChain, votar al principio pesa mas que votar al final.

La arquitectura general

El repositorio esta dividido en cuatro piezas principales:

  • Contratos Solidity en remix/: el contrato principal ZKTimeDecayVoting, un verificador mock de Groth16 y un stub de Poseidon para la demo.
  • Circuito ZK en circuits/vote.circom: circuito de referencia que demuestra pertenencia a un arbol Merkle sin revelar la identidad del votante.
  • Frontend HTML en docs/ y remix/: una interfaz de demo para conectar MetaMask, registrar credenciales, votar y ver resultados.
  • Relayer Node.js en relayer/: servidor Express que puede enviar transacciones por el votante para que su direccion no quede vinculada al voto.

La demo se penso para hackathon: facil de desplegar en Remix, probar sobre Sepolia y explicar en pocos minutos. Aun asi, el diseno detras toca temas bastante serios: commitments, nullifiers, Merkle trees, zk-SNARKs, relayers y threat modeling.

Fase 1: registro con commitments

El votante no se registra poniendo su identidad en la blockchain. En su lugar genera dos valores privados:

  • nullifier
  • secret

Con esos dos valores calcula un commitment:

commitment = hash(nullifier, secret)

Ese commitment se envia al contrato y se inserta como hoja en un Merkle tree incremental. En el contrato, el arbol esta configurado con profundidad 20, suficiente para alrededor de un millon de hojas.

La idea es sencilla pero potente: el contrato sabe que existe una lista de compromisos validos, pero no sabe que persona esta detras de cada compromiso. El votante debe guardar su nullifier y su secret, porque mas tarde los necesitara para demostrar que pertenece al arbol.

Fase 2: cierre del registro

Cuando ya se han registrado los votantes, el admin llama a closeRegistration(). Esto congela el estado de registro y deja fijada la raiz del Merkle tree.

Este paso es importante porque la prueba ZK se genera contra una raiz concreta. Si el conjunto de votantes cambiara durante la votacion, el sistema seria mucho mas dificil de razonar y podria abrir la puerta a manipulaciones.

El contrato tambien limita el poder del admin: parametros como verifier, hasher, votingStart, votingEnd y numChoices son immutable. Es decir, se fijan en el constructor y no se pueden cambiar silenciosamente a mitad de proceso.

Fase 3: prueba zero-knowledge

La parte mas interesante es la prueba ZK. El circuito vote.circom define lo que el votante debe probar sin revelar informacion sensible:

  1. Que conoce un nullifier y un secret.
  2. Que con ellos se puede calcular un commitment.
  3. Que ese commitment pertenece al Merkle tree congelado.
  4. Que el nullifierHash publico corresponde a su nullifier.
  5. Que la prueba queda ligada a una opcion de voto concreta.

La frase mental seria:

"Se demostrar que soy un votante registrado, pero no te digo cual."

El circuito usa Poseidon, un hash muy habitual en entornos zero-knowledge porque es mucho mas amigable para circuitos que hashes tradicionales como SHA-256.

Fase 4: nullifier para evitar doble voto

La privacidad por si sola no basta. Si nadie sabe quien eres, tambien hay que evitar que votes diez veces.

Para eso aparece el nullifierHash:

nullifierHash = hash(nullifier)

El contrato guarda cada nullifierHash usado en usedNullifiers. Si alguien intenta reutilizarlo, la transaccion revierte con NullifierAlreadyUsed.

Esto permite una propiedad muy importante: el voto sigue siendo anonimo respecto a la identidad del votante, pero el sistema puede detectar si la misma credencial intenta votar mas de una vez.

Fase 5: relayer para no vincular wallet y voto

Aunque la prueba ZK oculte la identidad criptografica del votante, si la persona envia la transaccion desde su propia wallet, la direccion queda en la blockchain. En muchos contextos eso ya seria suficiente para romper parte de la privacidad.

Por eso el repo incluye un relayer en Node.js. El relayer recibe:

  • prueba ZK,
  • raiz Merkle,
  • nullifierHash,
  • opcion votada,
  • datos de la proof de Groth16.

Y envia la transaccion desde su propia wallet. Asi, el votante no necesita aparecer como msg.sender del voto ni pagar gas directamente.

El relayer es semi-confiable, pero no deberia poder alterar el voto si la proof real esta bien integrada, porque la prueba queda ligada a los inputs publicos. En la demo, el relayer tambien incluye endpoints utiles como /api/status, /api/register, /api/relay-vote y /api/results.

Fase 6: peso temporal del voto

El contrato ZKTimeDecayVoting calcula el peso del voto en funcion del momento en que se emite.

La implementacion actual usa una formula lineal:

weight = (votingEnd - now) / (votingEnd - votingStart)

Al inicio de la votacion, el peso se aproxima al 100%. Conforme pasa el tiempo, baja hacia 0%. El contrato incluye ademas un minimo del 5% (MIN_WEIGHT) para evitar votos con peso insignificante.

Este diseno hace que los votos tempranos tengan mas influencia. No porque sean "mejores" por definicion, sino porque el sistema premia a quien decide antes de conocer la tendencia final.

Que hace el contrato principal

El contrato ZKTimeDecayVoting.sol concentra la logica del sistema:

  • Inicializa un Merkle tree incremental.
  • Registra commitments con registerVoter.
  • Cierra el registro con closeRegistration.
  • Verifica que la raiz Merkle sea conocida.
  • Rechaza nullifiers ya usados.
  • Verifica la proof Groth16 mediante un contrato verifier.
  • Calcula el peso temporal.
  • Suma el voto ponderado a voteTally.
  • Expone resultados con getResults.

Tambien emite eventos importantes:

  • VoterRegistered
  • RegistrationClosed
  • VoteCast

Eso permite auditar el proceso desde fuera y construir interfaces encima del contrato.

La demo y la limitacion honesta del hackathon

Hay un matiz importante: la demo del hackathon usa MockGroth16Verifier, un contrato que siempre devuelve true. Esto permite probar el flujo completo sin compilar el circuito, generar claves, hacer trusted setup y exportar un verificador real con snarkjs.

Tambien se usa PoseidonT3Stub, que sustituye Poseidon por keccak256 % BN128_FIELD para facilitar la demo en Remix.

Esto no invalida el proyecto; al contrario, muestra una decision de alcance muy tipica de una hackathon. En unas horas, priorizamos demostrar:

  • registro de votantes,
  • commitments,
  • arbol Merkle incremental,
  • cierre de registro,
  • voto con nullifier,
  • prevencion de doble voto,
  • recuento ponderado,
  • frontend funcional,
  • relayer opcional,
  • arquitectura ZK preparada.

Para produccion, habria que sustituir las piezas mock por:

  • circuito compilado con circom,
  • trusted setup con snarkjs,
  • Groth16Verifier.sol real,
  • Poseidon real compatible con el circuito,
  • generacion de proof en frontend,
  • pruebas de seguridad y auditoria del contrato.

Ese punto me gusta explicarlo con claridad porque da mas credibilidad tecnica: FadeChain no se presenta como un sistema listo para elecciones reales, sino como un prototipo avanzado que demuestra una arquitectura viable.

Modelo de amenazas

El repo tambien documenta varios ataques y mitigaciones:

  • Doble voto: mitigado con usedNullifiers.
  • Votante falso: en el diseno completo, mitigado con prueba de pertenencia Merkle.
  • Vincular wallet con voto: mitigado usando relayer.
  • Manipulacion de parametros: mitigado con variables immutable.
  • Manipulacion de timestamp: limitada por las reglas de timestamp de Ethereum PoS y por el peso minimo.
  • Front-running o mempool leakage: aceptado como limitacion en Sepolia; para mainnet se podria combinar relayer con mempool privado.

Esta parte fue clave para la presentacion, porque no basta con decir "usamos blockchain". Hay que explicar que amenazas existen y que propiedades protege cada pieza.

Por que gano

Creo que FadeChain funciono bien por tres motivos:

  1. Tenia un caso de uso claro: gobernanza privada y auditable para DAOs, presupuestos participativos o votaciones digitales.
  2. Combinaba varias capas reales: Solidity, Ethereum Sepolia, Merkle trees, ZK, frontend y relayer.
  3. Reconocia sus limites: demo con mock verifier, pero con circuito Circom y camino claro hacia produccion.

En una hackathon, no gana solo la idea mas ambiciosa. Gana la propuesta que se puede explicar, demostrar y defender tecnicamente. FadeChain tenia las tres cosas.

Lo que aprendi

Este proyecto me dejo varias lecciones:

  • Un buen sistema blockchain no consiste en poner todo on-chain, sino en decidir que debe ser publico y que debe seguir privado.
  • Zero-knowledge no es magia: requiere commitments, nullifiers, arboles Merkle, circuitos y verificadores que encajen entre si.
  • La privacidad tiene muchas capas. Ocultar la identidad en la proof no sirve de mucho si despues expones la wallet del votante.
  • En un prototipo serio hay que distinguir entre demo funcional y sistema productivo.
  • Explicar bien la arquitectura es casi tan importante como construirla.

Puedes ver el codigo en el repositorio de FadeChain o leer la ficha del proyecto en mi portfolio: FadeChain, proyecto ganador de hackathon blockchain UPF.