如何用Node.js实现DeVote链上投票系统?

来源:站长查询作者:台湾程序员头衔:程序员
导读:本期聚焦于台湾程序员创作的《如何用Node.js实现DeVote链上投票系统?》,敬请观看详情。怎样才能让投票结果不被任何人篡改?如果中心化数据库中的票数可以被管理员或入侵者修改,选举就失去了意义。DeVote 是一个使用 Node.js 和以太坊智能合约构建的链上投票系统,每一张选票都记录在区块链上,公开可验证且不可篡改。本文从系统架构、合约开发、Node.js 后端接入和前端交互四个层面,展示如何实现一个最小可用的去中心化投票应用。你需要准备 Node.js 18 以上版本、MetaMask 钱包、Hardhat 开发框架和 ethers.js 库。核心思路是把投票规则写入 Solidity 合约,再通过 Node.js 服务调用合约方法完成候选人注册、投票和实时统计。合约限制每个地址只能投一次,并用事件记录每次投票日志,方便前端同步。相比传统方案,链上投票虽然会消耗 Gas,但换来了数据透明和抗审查能力。读完本文你可以直接复制示例代码,在本地测试网络跑通完整流程。

DeVote 是一个去中心化投票系统,它的后端使用 Node.js,数据层使用以太坊智能合约。传统的投票系统通常把选票存在关系型数据库里,管理员拥有最高权限,可以修改或删除记录。DeVote 则把候选人和选票状态存放在合约存储中,任何一次投票都会生成交易并被打包进区块,节点可以独立验证。Node.js 在系统中扮演中间层,负责解析请求、调用合约读写方法、监听事件并向客户端返回格式化数据。

如何用Node.js实现DeVote链上投票系统?

一、系统架构与核心依赖

DeVote 的整体架构可以分为三层:以太坊节点层、Node.js 服务层和客户端展示层。以太坊节点负责维护合约状态、执行交易和广播事件;Node.js 服务通过 ethers.js 库连接节点,把合约的方法封装成 RESTful 接口;客户端则调用这些接口完成候选人注册、投票和结果查看。这种分层方式让合约逻辑与 Web 服务解耦,即使前端框架发生变化,合约和 Node.js 服务依然可以复用。

在开始编码之前,需要安装几个基础依赖。Node.js 版本建议使用 18 或更高版本,因为 ethers.js 6.x 依赖现代 JavaScript 特性。合约开发使用 Hardhat,它提供了本地测试网络、编译工具和部署脚本。初始化项目后,安装 ethers.js 和 express 两个核心包。ethers.js 负责与区块链交互,express 用来提供 HTTP 接口。本地测试时可以使用 Hardhat Network,不需要同步真实的以太坊主网,也不需要购买测试代币。

配置文件方面,Node.js 服务需要读取三个关键环境变量:RPC_URL、PRIVATE_KEY 和 CONTRACT_ADDRESS。RPC_URL 指向以太坊节点的访问地址,例如本地 Hardhat 节点的 http://127.0.0.1:8545。PRIVATE_KEY 是部署合约或发送交易的账户私钥,绝对不要提交到代码仓库。CONTRACT_ADDRESS 是合约部署后的链上地址,由部署脚本自动生成或手动写入。将这些变量放在 .env 文件中,通过 dotenv 加载,可以避免敏感信息泄露。

二、使用Solidity编写投票合约

智能合约是 DeVote 的核心,它定义了候选人结构、投票规则和事件日志。合约使用 Solidity 编写,部署后代码不可修改,因此需要在部署前充分测试。以下是一个最小可用的投票合约,它包含候选人注册、投票和查询功能。候选人信息通过 struct 类型保存,包含 id、名称和得票数。mapping 结构把候选人 id 映射到具体数据,同时记录每个地址是否已经投过票。

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

contract DeVote {
    struct Candidate {
        uint id;
        string name;
        uint voteCount;
    }

    mapping(uint => Candidate) public candidates;
    mapping(address => bool) public hasVoted;
    uint public candidatesCount;

    event VoteCast(uint indexed candidateId, address indexed voter);

    function addCandidate(string memory name) public {
        candidatesCount++;
        candidates[candidatesCount] = Candidate(candidatesCount, name, 0);
    }

    function vote(uint candidateId) public {
        require(!hasVoted[msg.sender], "Already voted");
        require(candidateId > 0 && candidateId <= candidatesCount, "Invalid candidate");
        candidates[candidateId].voteCount++;
        hasVoted[msg.sender] = true;
        emit VoteCast(candidateId, msg.sender);
    }

    function getCandidate(uint candidateId) public view returns (uint, string memory, uint) {
        Candidate memory c = candidates[candidateId];
        return (c.id, c.name, c.voteCount);
    }
}

合约中的 vote 函数包含两个 require 校验:第一个检查调用者是否已经投过票,第二个检查候选人 id 是否在有效范围内。这种写法可以防止同一个地址重复投票,也能避免传入不存在的候选人 id 导致异常。所有状态修改都是在交易被矿工打包后生效,因此投票操作具有最终性。事件 VoteCast 会在投票成功时触发,前端可以通过监听该事件实时更新界面。

部署合约时,可以使用 Hardhat 的部署脚本。脚本需要获取签名者账户,然后调用合约工厂的 deploy 方法。部署完成后,合约地址会打印在控制台。对于本地测试网络,每次重启节点后合约地址都会重置,因此 Node.js 服务应该在节点启动后读取最新地址。如果在测试网络上部署,需要保证账户中有足够的测试代币来支付 Gas 费用。

三、Node.js后端连接以太坊

Node.js 服务通过 ethers.js 的 JsonRpcProvider 连接以太坊节点。Provider 负责读取链上数据,例如查询候选人信息和当前票数。如果只执行读操作,不需要提供账户私钥。但 DeVote 需要添加候选人和投票,这些操作会改变合约状态,必须使用一个带私钥的钱包对象来签名交易。wallet 需要连接到 provider,这样它才能广播交易并等待确认。

const { ethers } = require("ethers");
require("dotenv").config();

const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const contractAddress = process.env.CONTRACT_ADDRESS;
const contractABI = require("./DeVoteABI.json");

const contract = new ethers.Contract(contractAddress, contractABI, wallet);

async function addCandidate(name) {
    const tx = await contract.addCandidate(name);
    await tx.wait();
    return tx.hash;
}

async function getCandidates() {
    const count = await contract.candidatesCount();
    const result = [];
    for (let i = 1; i <= count; i++) {
        const candidate = await contract.getCandidate(i);
        result.push({
            id: Number(candidate[0]),
            name: candidate[1],
            voteCount: Number(candidate[2])
        });
    }
    return result;
}

ABI 是合约的应用程序二进制接口,它描述了合约中有哪些函数、参数类型和返回值。Hardhat 编译合约后会在 artifacts 目录生成包含 ABI 的 JSON 文件,Node.js 服务可以直接引用。如果合约升级或修改,ABI 文件也需要同步更新,否则调用会失败。上面的 getCandidates 函数遍历所有候选人,把链上返回的大整数转换为 JavaScript 的 Number 类型,避免前端解析出现问题。

写操作和读操作的区别非常重要。addCandidate 和 vote 会产生交易,需要消耗 Gas,并且不会立即返回最终状态。tx.wait() 会等待交易被包含进区块,适合需要确认结果的场景。读操作如 candidatesCount 和 getCandidate 不会消耗 Gas,也不会修改链上数据,调用后可以立即得到结果。Node.js 服务应该根据接口性质选择对应的调用方式。

四、投票接口与防重复机制

为了让前端方便调用,Node.js 服务需要把合约方法封装成 HTTP 接口。添加候选人接口接收 name 参数,投票接口接收 candidateId 参数。在投票接口中,除了调用合约的 vote 方法,还可以在服务端做一层预检查,例如先查询 hasVoted 状态,如果该钱包已经投过票,直接返回 400 错误,避免产生不必要的 Gas 消耗。这种预检查不能代替合约层的校验,因为多个请求并发时仍可能产生竞争,但可以提升用户体验。

app.post("/vote", async (req, res) => {
    const candidateId = Number(req.body.candidateId);
    try {
        const voted = await contract.hasVoted(wallet.address);
        if (voted) {
            return res.status(400).json({ error: "This wallet has already voted" });
        }
        const tx = await contract.vote(candidateId);
        await tx.wait();
        res.json({ success: true, txHash: tx.hash });
    } catch (err) {
        console.error(err);
        res.status(400).json({ error: err.message });
    }
});

实际项目中,投票接口通常需要与用户身份绑定。如果所有投票都使用同一个服务端钱包签名,那么 hasVoted 状态只能记录服务端钱包是否投过票,无法区分不同用户。解决方式有两种:一种是让前端通过 MetaMask 直接签名交易,Node.js 只提供合约地址和 ABI;另一种是为每个用户创建一个链上钱包,由服务端管理私钥,但这样做安全成本较高。对于演示和学习目的,使用服务端钱包已经足够,重点在于理解合约状态变化和事件机制。

Gas 费用是链上投票的一个重要考虑因素。每次投票都会产生交易费,如果部署在以太坊主网,成本可能较高。开发阶段可以使用测试网络或本地节点,Gas 费用几乎为零。正式上线时,可以选择 Polygon、Arbitrum 等 Gas 费用较低的兼容 EVM 链。合约代码本身不需要修改,只需要在 Node.js 配置中修改 RPC_URL 和重新部署合约即可。

五、监听事件与结果统计

以太坊合约可以通过事件向外部应用推送状态变化。DeVote 合约中定义了 VoteCast 事件,投票成功时会自动触发。Node.js 服务使用 contract.on 方法订阅该事件,一旦收到新区块中的日志,就执行回调函数。回调函数可以更新内存中的缓存,或者通过 WebSocket 推送给前端,让前端在不刷新页面的情况下看到最新票数。事件监听不会消耗 Gas,是获取实时数据的高效方式。

contract.on("VoteCast", (candidateId, voter) => {
    console.log(`Candidate ${candidateId} received vote from ${voter}`);
    updateLocalCache(Number(candidateId));
});

事件监听需要保持 WebSocket 连接,某些 RPC 提供商可能不支持 WebSocket,可以使用轮询作为替代方案。轮询方式每隔一段时间调用 getCandidates 获取最新票数,实现简单但实时性较差。事件日志的另一个优势是历史可追溯,即使服务端重启,也可以从最早的区块开始重新扫描日志,恢复完整的投票记录。前端可以通过查询过滤条件,只获取某些候选人相关的事件。

统计结果时,不应该只依赖前端展示的票数,因为前端数据可能被篡改。最可靠的方式是每次查询都直接调用合约的 getCandidate 方法,读取链上存储的票数。Node.js 服务可以提供一个聚合接口,一次性返回所有候选人的 id、名称和票数,前端负责渲染成表格或图表。对于大规模投票,还可以把统计数据缓存到 Redis 中,通过监听事件刷新缓存,降低链上查询压力。

经过以上几个步骤,一个完整的 DeVote 链上投票系统就可以运行起来。这个系统证明了 Node.js 与智能合约结合并不复杂,关键是理解合约状态、交易签名、事件监听和 Gas 成本这些区块链特有的概念。把这个基础项目作为起点,你可以扩展候选人审核、投票时间窗口、匿名投票等功能,逐步构建更完善的去中心化应用。

Node.jsDeVote链上投票修改时间:2026-08-30 12:40:02

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。