1、需求分析
1.1 用户群体分析
需要进行投票表决的学校小团体、商业董事会群体、*人员等。
1.2 系统用途概述
本系统是一个基于区块链技术的线上投票系统。针对目前线上投票系统中出现的恶意刷票、数据安全、隐私泄露等问题,本系统结合区块链技术的去中心化、数据不可篡改、可匿名性等特点,可以保证投票系统的投票数据公正、公开、可验证、不可篡改,提高投票系统的可信任性。用户可以通过本系统进行投票项目的创建、规定投票时间、注册投票等操作,系统同时提供投票数据可追溯可验证的功能,旨在给用户构建一个更加高效安全的投票环境。
1.3 功能模块分析及数据要求
本系统的功能模块分为八个模块,如图1所示,分别为:登录模块、投票项目内容模块、注册码模块、起止时间模块、通知模块、注册模块、投票模块和结果模块。
1.3.1 登录模块
主要功能:可以通过该模块登录系统,若输错数据,则有弹出框提示。
所需数据:用户的以太坊账号和密钥。
1.3.2 投票内容模块
主要功能:可以通过该模块初始化投票项目的标题和选项内容。
所需数据:当前用户的以太坊账号,投票项目的标题和内容。
1.3.3 注册码模块
主要功能:可以通过该模块设置用户成为合格投票者所需的注册码。该模块需要加入加密算法功能,将注册码加密传输到服务区端。
所需数据:当前用户的以太坊账号,随机生成的注册码。
1.3.4 起止时间模块
主要功能:可以通过该模块设置投票者的注册起止时间和投票起止时间。
所需数据:当前用户的以太坊账号,注册起止时间、投票起止时间。
1.3.5 通知时间模块
主要功能:实时显示通过注册和投票的人数。
所需数据:当前用户的以太坊账号,注册的人数、投票的人数。
1.3.6 注册模块
主要功能:投票者通过该模块注册成为具有投票资格的投票者,若注册不通过会有弹出框。
所需数据:当前用户的以太坊账号,正确的注册码。
1.3.7 投票模块
主要功能:投票者通过该模块给候选者投票。
所需数据:当前用户的以太坊账号,投票内容。
1.3.8 结果模块
主要功能:在所有人都投完票或者投票截止时间到之后,该模块显示最终结果。
所需数据:投票项目的标题,候选者以及对应的票数。
图1. 功能模块示意图
2、总体设计
2.1 基本设计思路
本项目在以太坊的平台搭建,使用truffle开发测试框架,系统使用的网络环境为Ganache,并通过metamask进行交易。通过编写投票的智能合约,将合约部署到以太坊平台运行,实现对底层区块链的交互。所有后台的操作对用户透明,管理员和投票者只需通过前端的UI界面进行操作。
2.2 目录结构
在使用truffle框架的基础上,我们首先定下了需要使用的语言和框架:合约编写语言Solidity,前端框架bootstrap,后台与合约进行交互操作的web3js。 接着,在以上基础上,我们将项目大体上分为后端和前端,后端存放在js目录之下:
前端存放在src的目录下:
静态资源存放在:
基础配置以及注解都写在truffle-config.js中.
2.3 测试计划
在系统功能实现的过程中需要进行各种例子的测试,首先需要保证基本功能的实现,然后根据各种合法与非法的例子测试,总结出当前功能存在的问题,再进行修正,再测试,直到找不出问题为止。
3、详细设计
3.1 系统架构
图2是基于以太坊的去中心化的架构图,从图中我们可以看出区块链层的工作主要是对合约运行过程中产生的数据进行存储,以太坊平台提供了合约的编译和部署环境以及对区块链进行交互的接口,对数据的操作只需调用各种方法接口,而不用直接对区块链本身进行编码工作。前端与合约进行交互主要通过Web3js,Web3js主要任务就是对WEB前端用户的操作和合约内容进行连接,因此该处有大量的调度函数,里面定义了来自用户的各种功能要求函数,以及来自区块链层的各种数据返回指令。Web前端层的任务就是将系统的功能以图形界面的方式展现给使用该系统的人,因此在用户层的主要内容就是书写大量的html页面,通过各种消息触发方式实现所提到的需求。
3.2 管理员操作
管理员登录系统的流程图如图3右侧部分所示,首先管理员用管理员账号登录系统,同时登录metamask钱包进行交易,账号正确则进入投票项目内容模块。管理员开始设置项目的标题和选项内容,确认交易后进入注册码模块;管理员将加密的注册码传给后台,确认交易进入起止时间模块;管理员设置系统的注册起止时间和投票起止时间,随后确认交易进入通知模块;通知模块显示目前通过注册和投票的用户人数,若在规定时间内投票者都投完了票或者时间超过了投票的截止时间,投票结束并公布投票结果。
图2. 基于以太坊的去中心化架构
在上面提到的所有操作中,如果管理员用户更换metamask的账户,则交易失败,返回系统登录界面。
图3. 管理员和投票者的操作流程图
3.3 投票者操作
投票者的流程图如图3左侧部分所示,首先投票者用自己的以太坊账户登录系统,同时登录metamask钱包进行交易,账号正确则进入投票模块;如果投票者未注册投票项目,则投票者进入注册模块传输加密的注册码进行注册,若注册码正确,则投票者返回投票模块,选择自己意向的候选者进行投票,若该投票者是第一次投票,则投票成功且返回投票项目的截止时间;若不是则投票失败。若用户在投票截止时间之后登录系统,则直接跳转到公布投票结果模块。
在上面提到的所有操作中,如果用户在注册前更换metamask的账户,系统不受影响;若在注册后更换metamask账户,则新账户需要注册,但是由于用户使用注册码将之前的账户注册成功,所以新账户再次使用注册码会注册失败,且后续操作也不能完成。
4、编码实现
附录列举了投票模块的代码。
5、结果展示
5.1 登录
5.2 设置初始化投票项目
5.3 设置注册码
5.4 设置注册和投票的起止时间
5.5 通知模块
5.5 用户登录
5.6 用户注册
5.7 用户投票
5.8 结果
6、总结
本次报告说明了基于区块链投票系统的需求分析、总体设计、详细设计、编码实现和结果展示。通过本次实验我收获了许多关于如何在以太坊平台搭建DApp的知识,我发现前期多看书籍多查找资料的话,后期的编码会很顺利。下一步就是正式开始撰写毕设论文啦。
附录:
投票模块的前端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>区块链投票</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<!-- for pkcs5pkey -->
<script src="components/googlecode/core.js"></script>
<script src="components/googlecode/cipher-core.js"></script>
<script src="components/googlecode/md5.js"></script>
<script src="components/googlecode/tripledes.js"></script>
<script src="components/googlecode/enc-base64.js"></script>
<!-- for crypto -->
<script src="components/googlecode/sha1.js"></script>
<script src="components/googlecode/sha256.js"></script>
<!-- for crypto, asn1, asn1x509 -->
<script src="components/yahoo/yahoo-min.js"></script>
<!-- for asn1x509(stohex)
<script src="http://kjur.github.io/jsjws/base64x-1.1.min.js"></script> -->
<script language="JavaScript" type="text/javascript" src="ext/jsbn.js"></script>
<script language="JavaScript" type="text/javascript" src="ext/jsbn2.js"></script>
<script language="JavaScript" type="text/javascript" src="ext/prng4.js"></script>
<script language="JavaScript" type="text/javascript" src="ext/rng.js"></script>
<script language="JavaScript" type="text/javascript" src="ext/rsa.js"></script>
<script language="JavaScript" type="text/javascript" src="ext/rsa2.js"></script>
<script language="JavaScript" type="text/javascript" src="ext/base64.js"></script>
<script language="JavaScript" type="text/javascript" src="asn1hex-1.1.js"></script>
<script language="JavaScript" type="text/javascript" src="rsapem-1.1.js"></script>
<script language="JavaScript" type="text/javascript" src="rsasign-1.2.js"></script>
<script language="JavaScript" type="text/javascript" src="x509-1.1.js"></script>
<script language="JavaScript" type="text/javascript" src="pkcs5pkey-1.0.js"></script>
<script language="JavaScript" type="text/javascript" src="asn1-1.0.js"></script>
<script language="JavaScript" type="text/javascript" src="asn1x509-1.0.js"></script>
<script language="JavaScript" type="text/javascript" src="components/sm2/crypto-1.1.js"></script>
<script language="JavaScript" type="text/javascript" src="ext/ec.js"></script>
<script language="JavaScript" type="text/javascript" src="ext/ec-patch.js"></script>
<script language="JavaScript" type="text/javascript" src="ecdsa-modified-1.0.js"></script>
<script language="JavaScript" type="text/javascript" src="components/sm2/sm3.js"></script>
<script language="JavaScript" type="text/javascript" src="components/sm2/sm3-sm2-1.0.js"></script>
<script language="JavaScript" type="text/javascript" src="components/sm2/ecparam-1.0.js"></script>
<script language="JavaScript" type="text/javascript" src="components/sm2/sm2.js"></script>
<script language="JavaScript" type="text/javascript">
/*
function doGenerate() {
var f1 = document.form1;
var curve = f1.curve1.value;
var ec = new KJUR.crypto.ECDSA({"curve": curve});
var keypair = ec.generateKeyPairHex();
f1.prvkey1.value = keypair.ecprvhex;
f1.pubkey1.value = keypair.ecpubhex;
}
*/
function doCrypt() {
var f1 = document.form1;
var curve = f1.curve1.value;
var msg = f1.msg1.value;
var msgData = CryptoJS.enc.Utf8.parse(msg);
var pubkeyHex = f1.pubkey1.value;
if (pubkeyHex.length > 64 * 2) {
pubkeyHex = pubkeyHex.substr(pubkeyHex.length - 64 * 2);
}
var xHex = pubkeyHex.substr(0, 64);
var yHex = pubkeyHex.substr(64);
var cipherMode = f1.cipherMode.value;
var cipher = new SM2Cipher(cipherMode);
var userKey = cipher.CreatePoint(xHex, yHex);
msgData = cipher.GetWords(msgData.toString());
var encryptData = cipher.Encrypt(userKey, msgData);
f1.sigval1.value = encryptData;
}
/*function doDecrypt() {
var f1 = document.form1;
var prvkey = f1.prvkey1.value;
var encryptData = f1.sigval1.value;
var privateKey = new BigInteger(prvkey, 16);
var cipherMode = f1.cipherMode.value;
var cipher = new SM2Cipher(cipherMode);
var data = cipher.Decrypt(privateKey, encryptData);
alert(data ? '解密成功,原文:' + data : '解密失败!');
}
function certCrypt() {
var certData = document.getElementById('txtCertData').value;
if( certData != "") {
var key = X509.getPublicKeyFromCertPEM(certData);
document.getElementById('txtPubKey').value = key.pubKeyHex;
}
var pubkey = document.getElementById('txtPubKey').value.replace(/\s/g,'');
var pubkeyHex = pubkey;
if (pubkeyHex.length > 64 * 2) {
pubkeyHex = pubkeyHex.substr(pubkeyHex.length - 64 * 2);
}
var xHex = pubkeyHex.substr(0, 64);
var yHex = pubkeyHex.substr(64);
var cipherMode = document.getElementById('cipherMode').value;
var cipher = new SM2Cipher(cipherMode);
var userKey = cipher.CreatePoint(xHex, yHex);
var msg = document.getElementById('txtRawData').value;
var msgData = CryptoJS.enc.Utf8.parse(msg);
msgData = cipher.GetWords(msgData.toString());
var encryptData = cipher.Encrypt(userKey, msgData);
document.getElementById('txtCryptData').value = hex2b64(encryptData);
}*/
</script>
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="css/form-validation.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container">
<div class="py-5 text-center">
<img src="images/title1.svg" alt="" width="" height="">
<h2 id="projectName"> </h2>
</div>
<div class="table-responsive">
<table class="table table-bordered" >
<thead>
<tr>
<th >选项</th>
</tr>
</thead>
<tbody>
<tr>
<td id="select1"></td>
</tr>
<tr>
<td id="select2"></td>
</tr>
<tr>
<td id="select3"></td>
</tr>
</tbody>
</table>
</div>
需要先注册才能进行投票哦,点击<a href="login.html">这里</a>进行注册!
<hr class="mb-4">
<!-- MAIN CONTENT -->
<div id="main_content_wrap" class="outer">
<section id="main_content" class="inner">
<!-- now editing -->
<form name="form1">
<div class="mb-3">
<label for="note">请按以下步骤进行投票:</label><br>
<label>1、系统生成的公钥</label><br/>
椭圆曲线加密名称:
<select name="curve1">
<option value="sm2">SM2
</select>
<!--input type="button" value="生成密钥对" onClick="doGenerate();"/><br/-->
<br>
<!--私钥 (十六进制): <input type="text" class="form-control" name="prvkey1" id="prvkey1" /-->
公钥 (十六进制): <input type="text" class="form-control" name="pubkey1" id="pubkey1" />
</div>
<hr class="mb-4">
<!-- ============================================================== -->
<label for="sig">2、加密选票</label><br>
加密方式:
<select id="cipherMode" name="cipherMode">
<option value="1" selected="selected">C1C3C2
</select><br/>
需要加密的选票内容:
<input type="text" name="msg1" class="form-control" value="" />
<input type="button" value="加密选票" onClick="doCrypt();"/><br/>
<hr class="mb-4">
<label for="data">3、加密后的选票 (十六进制)</label><br>
<p>
<input type="text" id="sigval1" class="form-control" name="sigval1" value="" />
</p>
<!--
<button class="btn btn-default btn-vote" type="button" >注册</button>
-->
</form>
<hr class="mb-4">
<button class="btn btn-primary btn-lg btn-block" type="submit">投票</button>
<!--
<button class="btn btn-default btn-vote" type="button" >投票</button><br>
-->
<!-- now editing -->
</section>
</div>
</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="js/web3.min.js"></script>
<script src="js/truffle-contract.js"></script>
<script src="js/vote.js"></script>
<script src="js/bootstrap.min.js"></script>
</body>
</html>
<style>
.test-center{
text-align: center;
}
table{
width: 50%;
}
table tr{
border-bottom: 2px solid #efefef;
height: 40px;
}
</style>
投票模块的后端代码:
Vote = {
web3Provider: null,
contracts: {},
initWeb3: function () {
if (typeof web3 !== 'undefined') {
Vote.web3Provider = web3.currentProvider;
} else {
Vote.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
}
web3 = new Web3(Vote.web3Provider);
Vote.initContract();
},
initContract: function () {
$.getJSON('Voting.json', function (data) {
var Artifact = data;
Vote.contracts.Voting = TruffleContract(Artifact);
Vote.contracts.Voting.setProvider(Vote.web3Provider);
//console.log(Vote.contracts.Voting);
//Vote.setCounts();
});
Vote.handleRegister();
},
handleRegister: function() {
var Instance;
//var registerId = $("#registerId").val();
//console.log(registerId);
// 获取用户账号
web3.eth.getAccounts(function (error, accounts) {
if (error) {
console.log(error);
}
var account = accounts[0];
//console.log(account);
//$("#user_id").html(account);
Vote.contracts.Voting.deployed().then(function (instance) {
Instance = instance;
Instance.getProjectName.call({from:account}).then((Vote) => {
console.log("Success! Got Vote: " + Vote);
var projectName = Vote;
//alert(projectName);
$("#projectName").html(projectName);
}).catch((err) => {
console.log("Failed with error: " + err);
});
//var projectName = Instance.getProjectName().then(value => console.log(value));
//get select1
Instance.getCandidateList.call(0,{from:account}).then((Vote) => {
console.log("Success! Got Vote: " + Vote);
var select1 = Vote;
//alert(select1);
$("#select1").html(select1);
}).catch((err) => {
console.log("Failed with error: " + err);
});
//get select2
Instance.getCandidateList.call(1,{from:account}).then((Vote) => {
console.log("Success! Got Vote: " + Vote);
var select2 = Vote;
//alert(select1);
$("#select2").html(select2);
}).catch((err) => {
console.log("Failed with error: " + err);
});
Instance.getCandidateList.call(2,{from:account}).then((Vote) => {
console.log("Success! Got Vote: " + Vote);
var select3 = Vote;
//alert(select1);
$("#select3").html(select3);
}).catch((err) => {
console.log("Failed with error: " + err);
});
});
});
Vote.doGenerate();
},
doGenerate: function(){
var f1 = document.form1;
var curve = f1.curve1.value;
var ec = new KJUR.crypto.ECDSA({"curve": curve});
var keypair = ec.generateKeyPairHex();
//f1.prvkey1.value = keypair.ecprvhex;
f1.pubkey1.value = keypair.ecpubhex;
var prvkey1 = keypair.ecprvhex+" ";
var pubkey1 = f1.pubkey1.value;
var Instance;
// 获取用户账号
web3.eth.getAccounts(function (error, accounts) {
if (error) {
console.log(error);
}
var account = accounts[0];
//alert(account);
//console.log(account);
Vote.contracts.Voting.deployed().then(function (instance) {
Instance = instance;
Instance.doGenerate(prvkey1, {from: account});
//$("#prvkey1").html(prvkey1);
$("#pubkey1").html(pubkey1);
});
});
//$("#pubkey1").html(keypair.ecpubhex);
Vote.vote();
},
vote: function () {
$(document).on('click', '.btn', Vote.handleVote);
},
handleVote: function () {
var f1 = document.form1;
var VotingInstance;
// 获取用户账号
web3.eth.getAccounts(function (error, accounts) {
if (error) {
console.log(error);
}
var account = accounts[0];
//console.log(accounts);
Vote.contracts.Voting.deployed().then(function (instance) {
VotingInstance = instance;
VotingInstance.VaildID.call({from:account}).then((vaildornot) => {
console.log("Success! Got Vote: " + vaildornot);
if(!vaildornot){
alert("请先注册!");
window.location.href="login.html";
}
else{
VotingInstance.isVoted.call({from:account}).then((votedornot) => {
console.log("Success! Got Vote: " + votedornot);
if(votedornot){
alert("你已投过票!");
Vote.initWeb3();
}
else{
//get privatekey
VotingInstance.getPrivateKey.call({from:account}).then((result) => {
console.log("Success! Got result: " + result);
var prvkey = result;
console.log(prvkey);
var privateKey = new BigInteger(prvkey, 16);
//var prvkey = f1.prvkey1.value;
//console.log(prvkey);
var encryptData = f1.sigval1.value;
console.log(encryptData);
var privateKey = new BigInteger(prvkey, 16);
console.log(privateKey);
var cipherMode = f1.cipherMode.value;
console.log(cipherMode);
var cipher = new SM2Cipher(cipherMode);
console.log(cipher);
var data = cipher.Decrypt(privateKey, encryptData);
console.log(data);
var candidateName = data;
VotingInstance.voteForCandidate(candidateName, {from: account});
VotingInstance.getVoteEndTime.call({from:account}).then((result) => {
console.log("Success! Got result: " + result);
//VoteEndTime = new Date(result.replace(/-/g,"\/"));
alert("投票成功!结果将会于"+ result +"公布!")
}).catch((err) => {
console.log("Failed with error: " + err);
});
});
}
}).catch((err) => {
console.log("Failed with error: " + err);
});
}
}).catch((err) => {
console.log("Failed with error: " + err);
});
});
});
}
};
$(function () {
$(window).load(function () {
Vote.initWeb3();
});
});