Votación Secreta
Construye un circuito de votación privada con elegibilidad de votantes, privacidad del voto y prevención de doble voto.
La votación secreta es una de las aplicaciones más naturales de las pruebas de conocimiento cero. Un votante demuestra que es elegible y emite un voto válido — sin revelar su identidad ni qué opción eligió. Un nulificador público evita que el mismo votante vote dos veces.
Conceptos
El circuito combina cuatro componentes:
- Compromiso del votante — cada votante registra un compromiso
poseidon(secret, 0)derivado de un secreto privado. El compromiso es público; el secreto no. - Membresía Merkle — todos los compromisos de votantes son hojas en un árbol Merkle. El votante demuestra que su compromiso está en el árbol sin revelar cuál hoja es.
- Nulificador —
poseidon(secret, election_id)produce un valor único y determinista vinculado tanto al votante como a la elección. Publicar el nulificador permite detectar duplicados sin saber quién votó. - Validez del voto —
range_check(vote, 1)asegura que el voto sea 0 o 1.
Definición del Circuito
Crea secret_vote.ach:
circuit secret_vote(merkle_root: Public, nullifier: Public, vote: Public, election_id: Public, secret: Witness, path: Witness Field[2], indices: Witness Bool[2]) {
// 1. Compromiso del votante (oculto — calculado desde el secreto privado)
let commitment = poseidon(secret, 0)
// 2. Membresía Merkle: el votante está en el árbol de votantes registrados
merkle_verify(merkle_root, commitment, path, indices)
// 3. Correctitud del nulificador: previene doble voto
let expected_nullifier = poseidon(secret, election_id)
assert_eq(expected_nullifier, nullifier)
// 4. Validez del voto: debe ser 0 o 1
range_check(vote, 1)
}
El compromiso se calcula dentro del circuito a partir del testigo secret. Nunca aparece como entrada pública — el verificador solo ve la raíz Merkle, el nulificador, el voto y el ID de la elección.
Construir el Registro de Votantes
Antes de ejecutar el circuito, necesitas un árbol Merkle de compromisos. Aquí un árbol de 4 votantes (profundidad 2) construido en modo VM:
// Cuatro secretos de votantes
let s0 = 0p42
let s1 = 0p111
let s2 = 0p222
let s3 = 0p333
// Compromisos
let c0 = poseidon(s0, 0)
let c1 = poseidon(s1, 0)
let c2 = poseidon(s2, 0)
let c3 = poseidon(s3, 0)
// Nivel 0: hashes de pares
let n0 = poseidon(c0, c1)
let n1 = poseidon(c2, c3)
// Nivel 1: raíz
let root = poseidon(n0, n1)
print(root)
Al ejecutar con ach run se obtiene la raíz:
ach run registry.ach
# Field(16562627490493722277540343453474560507943355785745140792129356826951042972366)
Calcular la Ruta de Prueba
El votante 0 (secreto 42) está en el índice 0 del árbol (binario 00). Su prueba Merkle contiene el hermano en cada nivel:
- Nivel 0:
c0es el hijo izquierdo, el hermano esc1(compromiso del votante 1). Índice = 0. - Nivel 1:
n0es el hijo izquierdo, el hermano esn1. Índice = 0.
Entonces path = [c1, n1] e indices = [0, 0].
Compilar el Circuito
ach circuit secret_vote.ach \
--inputs "merkle_root=16562627490493722277540343453474560507943355785745140792129356826951042972366,\
nullifier=4027913667401648903638418705764660665764112454358309045410324429160920395813,\
vote=1,election_id=1001,secret=42,\
path_0=6742193431752037917634653485837689273334250178444557194345979079134234961755,\
path_1=2479855382401079998356559563096754868958560665915964078751529288374953894653,\
indices_0=0,indices_1=0"
Costo de Restricciones
| Componente | Restricciones |
|---|---|
poseidon(secret, 0) — compromiso | 361 |
merkle_verify (profundidad 2) | ~726 |
poseidon(secret, election_id) — nulificador | 361 |
assert_eq — verificación del nulificador | 1 |
range_check(vote, 1) — validez del voto | 2 |
| Total | ~1,451 |
Con profundidad 20 (~1M votantes), el total sube a aproximadamente 8,100 restricciones — todavía muy rápido de probar.
Usando prove {} en su Lugar
Puedes generar la prueba en línea usando un bloque prove {}. El ámbito externo calcula el árbol Merkle en modo VM, y el bloque prove genera la prueba ZK:
let secret: Field = 0p42
let election_id: Field = 0p1001
let vote: Field = 0p1
// Construir registro de votantes
let commitment: Field = poseidon(secret, 0)
let voter1: Field = poseidon(0p111, 0p0)
let voter2: Field = poseidon(0p222, 0p0)
let voter3: Field = poseidon(0p333, 0p0)
let n0: Field = poseidon(commitment, voter1)
let n1: Field = poseidon(voter2, voter3)
let merkle_root: Field = poseidon(n0, n1)
let nullifier: Field = poseidon(secret, election_id)
// Prueba Merkle para votante 0
let path_0: Field = voter1
let path_1: Field = n1
let indices_0: Field = 0p0
let indices_1: Field = 0p0
prove(merkle_root: Public, nullifier: Public, vote: Public, election_id: Public) {
let commitment: Field = poseidon(secret, 0)
merkle_verify(merkle_root, commitment, path, indices)
let expected_nullifier: Field = poseidon(secret, election_id)
assert_eq(expected_nullifier, nullifier)
range_check(vote, 1)
}
Ejecuta con ach run secret_vote.ach. El bloque prove {} compila el circuito, genera el testigo, verifica las restricciones y produce una prueba — todo en un solo paso.
Escalar
- Más votantes: aumenta la profundidad del árbol Merkle. Profundidad 20 soporta ~1M votantes con ~8,100 restricciones.
- Múltiples candidatos: reemplaza
range_check(vote, 1)conrange_check(vote, N)donde N = ceil(log2(candidatos)). - Conteo: publica todos los pares
(nulificador, voto). Cualquiera puede verificar que no hay nulificadores duplicados y sumar los votos, sin saber quién emitió cada voto.
Aplicaciones
- Gobernanza DAO — los poseedores de tokens votan sin revelar su posición
- Encuestas anónimas — recopilar retroalimentación honesta con unicidad garantizada
- Elecciones on-chain — enviar pruebas a un verificador Solidity (
ach circuit --solidity)