tutoriales.com

Construyendo un Sistema de Votación Descentralizada Seguro con Solidity y Merkle Trees

Este tutorial práctico y avanzado te guía paso a paso en la creación de un contrato inteligente de votación en Ethereum. Descubrirás cómo implementar árboles de Merkle (Merkle Trees) para verificar listas de votantes de manera eficiente sin sobrecargar la red con costos excesivos de gas.

Avanzado9 min de lectura19 views
Reportar error

🗳️ Introducción a la Gobernanza Descentralizada y los Desafíos de Gas

La gobernanza en el ecosistema blockchain es uno de los pilares fundamentales para la creación de organizaciones verdaderamente autónomas y transparentes. Sin embargo, desarrollar contratos inteligentes de votación en Ethereum presenta un reto técnico mayúsculo: la gestión eficiente del gas y la privacidad de los votantes.

Tradicionalmente, almacenar una lista blanca (whitelist) de direcciones autorizadas para votar directamente en el almacenamiento (storage) de un contrato inteligente es prohibitivamente costoso. Imagina registrar a 10,000 votantes iterando y guardando cada dirección mediante un mapeo en Solidity; el costo en gas rompería cualquier presupuesto razonable. Aquí es donde entran en juego las estructuras de datos criptográficas, específicamente los Merkle Trees (Árboles de Merkle).

💡 Consejo: Los Merkle Trees permiten a los contratos inteligentes verificar la inclusión de un elemento en un conjunto masivo procesando únicamente una pequeña prueba criptográfica llamada Merkle Proof, ahorrando hasta un 90% en costos de gas comparado con arreglos en storage.

🛠️ Conceptos Fundamentales: ¿Qué es un Merkle Tree?

Un árbol de Merkle es un árbol binario en el que cada nodo hoja es el hash de un bloque de datos (en nuestro caso, una dirección de Ethereum combinada con un peso o identificador), y cada nodo no hoja es el hash de sus dos hijos concatenados. La raíz del árbol (Merkle Root) resume todo el conjunto de datos en un solo valor de 32 bytes.

Raíz de Merkle (Hash Final) Hash(A+B) Nodo Intermedio Hash(C+D) Nodo Intermedio Hash Votante A Hoja 1 Hash Votante B Hoja 2 Hash Votante C Hoja 3 Hash Votante D Hoja 4

Cuando un usuario quiere demostrar que su dirección está autorizada para votar, no necesita enviar toda la lista. Solo proporciona su dirección y una prueba de Merkle compuesta por los hashes hermanos necesarios para reconstruir el camino hasta la raíz. El contrato inteligente simplemente valida esta prueba utilizando la función criptográfica nativa de Ethereum: keccak256.


📐 Arquitectura del Sistema de Votación

Para diseñar nuestro contrato inteligente, definiremos las siguientes fases y componentes:

  1. Fase de Despliegue: Se inicializa el contrato estableciendo la merkleRoot, el tiempo de inicio y el tiempo de cierre de las votaciones.
  2. Fase de Votación: Los usuarios elegibles llaman a la función vote() proporcionando su voto y la prueba de Merkle (Merkle Proof).
  3. Fase de Conteo/Resultados: Se pueden consultar los resultados de manera transparente y descentralizada.
Paso 1: Generación Off-chain de la Merkle Root usando Node.js y ethers.js.
Paso 2: Despliegue del contrato inteligente Solidity con la Root configurada.
Paso 3: Ejecución de votos on-chain con validación de Merkle Proof.

💻 Implementación del Contrato Inteligente en Solidity

A continuación, desarrollaremos el código completo del contrato inteligente MerkleVoting.sol. Asegúrate de utilizar una versión reciente de Solidity (^0.8.20).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title MerkleVoting
 * @dev Sistema de votación descentralizada que utiliza Merkle Proofs para listas blancas eficientes.
 */
contract MerkleVoting {
    // Estructura para almacenar las opciones de votación
    struct Proposal {
        string name;
        uint256 voteCount;
    }

    // Raíz del árbol de Merkle que contiene las direcciones autorizadas
    bytes32 public immutable merkleRoot;

    // Mapeo para registrar qué direcciones ya han votado (evita doble voto)
    mapping(address => bool) public hasVoted;

    // Arreglo dinámico de propuestas
    Proposal[] public proposals;

    // Eventos para indexar la actividad del contrato
    event Voted(address indexed voter, uint256 indexed proposalIndex);

    // Modificador para verificar que la votación está activa
    uint256 public immutable votingStart;
    uint256 public immutable votingEnd;

    modifier onlyDuringVoting() {
        require(block.timestamp >= votingStart, "La votacion aun no ha comenzado");
        require(block.timestamp <= votingEnd, "La votacion ha finalizado");
        _;
    }

    constructor(bytes32 _merkleRoot, stringNames[] memory _proposalNames, uint256 _durationInSeconds) {
        merkleRoot = _merkleRoot;
        votingStart = block.timestamp;
        votingEnd = block.timestamp + _durationInSeconds;

        for (uint256 i = 0; i < _proposalNames.length; i++) {
            proposals.push(Proposal({
                name: _proposalNames[i],
                voteCount: 0
            }));
        }
    }

    /**
     * @notice Permite a un votante emitir su voto proporcionando una prueba de Merkle
     * @param proposalIndex Índice de la propuesta a la que se vota
     * @param merkleProof Arreglo de hashes que prueban la inclusión en la whitelist
     */
    function vote(uint256 proposalIndex, bytes32[] calldata merkleProof) external onlyDuringVoting {
        require(!hasVoted[msg.sender], "El usuario ya ha emitido su voto");
        require(proposalIndex < proposals.length, "Propuesta invalida");

        // Construir la hoja del arbol combinando la direccion del remitente
        bytes32 node = keccak256(abi.encodePacked(msg.sender));

        // Verificar la prueba de Merkle
        require(verify(merkleProof, merkleRoot, node), "Prueba de Merkle invalida: No estas autorizado");

        // Registrar el voto
        hasVoted[msg.sender] = true;
        proposals[proposalIndex].voteCount += 1;

        emit Voted(msg.sender, proposalIndex);
    }

    /**
     * @dev Función interna para verificar la prueba de Merkle
     */
    function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
        bytes32 computedHash = leaf;

        for (uint256 i = 0; i < proof.length; i++) {
            bytes32 proofElement = proof[i];

            if (computedHash <= proofElement) {
                computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
            } else {
                computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
            }
        }

        return computedHash == root;
    }

    function getProposalsCount() external view returns (uint256) {
        return proposals.length;
    }
}

🔍 Análisis Detallado del Código

Profundicemos en las secciones críticas del contrato inteligente para entender cómo opera a bajo nivel:

  • Inmutabilidad de la Merkle Root: Declaramos merkleRoot como immutable. Esto significa que su valor se asigna durante el despliegue del contrato y no puede modificarse jamás, garantizando que los organizadores no puedan alterar la lista de votantes a mitad del proceso.
  • Optimización con calldata: En la función vote, el argumento merkleProof utiliza la ubicación de datos calldata en lugar de memory. Esto reduce drásticamente el costo de gas al leer directamente los datos de la transacción sin copiarlos innecesariamente a la memoria RAM virtual de la EVM.
  • Ordenamiento Criptográfico: Dentro de la función verify, comparamos computedHash <= proofElement antes de concatenar y aplicar keccak256. Esto asegura que el orden de los nodos hermanos en el árbol coincida exactamente con la estructura generada fuera de la cadena (off-chain).
⚠️ Advertencia: Si generas tus hojas de Merkle usando librerías de JavaScript como openzeppelin/merkle-tree, asegúrate de que el formato de empaquetado (como `ethers.solidityPackedKeccak256`) coincida exactamente con `abi.encodePacked(msg.sender)` en Solidity. Un desajuste en el orden o tipo de datos invalidará todas las pruebas.

🌐 Generación del Árbol de Merkle Off-Chain (Script de Node.js)

Para interactuar con nuestro contrato, necesitamos generar la raíz (Merkle Root) y las pruebas individuales para cada votante utilizando Node.js y la librería oficial de OpenZeppelin.

const { StandardMerkleTree } = require("@openzeppelin/merkle-tree");
const ethers = require("ethers");

// Lista de direcciones de votantes autorizados
const values = [
    ["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"],
    ["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2"],
    ["0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db"]
];

// Construir el árbol de Merkle
const tree = StandardMerkleTree.of(values, ["address"]);

console.log("Merkle Root:", tree.root);

// Generar prueba para el primer votante
for (const [i, v] of tree.entries()) {
    if (v[0] === "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4") {
        const proof = tree.getProof(i);
        console.log("Prueba para Votante 1:", proof);
    }
}

📊 Comparativa de Costes de Gas: Whitelist Tradicional vs. Merkle Tree

Para justificar el uso de esta arquitectura avanzada, observemos la diferencia estimada en el consumo de gas para registrar y validar votantes en una red tipo Ethereum o capa 2 (como Arbitrum o Optimism).

Estrategia de WhitelistCosto de DespliegueCosto por Validación / VotoEscalabilidad (10,000 usuarios)
------------
Mapeo Tradicional (mapping(address => bool))Muy Bajo (~50,000 gas)Muy Bajo (~30,000 gas)Prohibitivamente caro en despliegue y actualización
Merkle Tree ProofMuy Bajo (~100,000 gas)Moderado (~65,000 gas)Infinitamente escalable sin modificar el contrato

❓ Preguntas Frecuentes (FAQ)

¿Qué pasa si pierdo mi Merkle Proof?Los organizadores de la votación o la aplicación descentralizada (dApp) frontend suelen alojar un servidor o base de datos ligera donde los usuarios pueden consultar su Merkle Proof de forma automática al conectar su billetera (como MetaMask).
¿Puedo añadir más votantes una vez desplegado el contrato?No directamente en el mismo árbol, ya que la `merkleRoot` es inmutable. Si necesitas añadir votantes, tendrías que desplegar un nuevo contrato o diseñar un patrón de actualización que permita cambiar la raíz mediante gobernanza.

🎯 Conclusión

El uso de Merkle Trees en combinación con Solidity representa una de las mejores prácticas para optimizar contratos inteligentes orientados a votaciones, airdrops y listas blancas masivas. Al desplazar el almacenamiento pesado fuera de la cadena (off-chain) y verificar únicamente pruebas criptográficas (on-chain), logramos sistemas económicos, seguros y verdaderamente descentralizados.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!