记录一次以太坊投票智能合约的调试过程
在看到这个代码以及编译部署方式后,就想着来执行下,但路途坎坷.
根据书上的说法,我把代码放进IDE,点这再点那,就可以了,但事实并没有这么简单。
记录下坎坷的过程。
打开IDE
Remix是官方推荐的智能合约开发IDE,适合新手,可以在浏览器中快速部署测试智能合约。打开这个网址就行(https://remix.ethereum.org)。
点击左上角小加号,添加名为Ballot.sol的文件
然后将书上的代码贴入,以下是源码:
pragma solidity ^0.4.0;
contract BallotPro {
/// 投票者Voter的数据结构
struct Voter {
uint weight; // 该投票者的投票所占的权重
bool voted; // 是否已经投过票
uint vote; // 投票对应的提案编号(Index)
address delegate; // 该投票者投票权的委托对象
}
/// 提案Proposal的数据结构
struct Proposal {
bytes32 name; // 提案的名称
uint voteCount; // 该提案目前的票数
}
/// 投票的主持人
address chairperson;
/// 投票者地址和状态的对应关系
mapping(address => Voter) voters;
/// 提案的列表
Proposal[] proposals;
/// 在初始化合约时,给定一个提案名称的列表
function BallotPro(bytes32[] proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1;
for (uint i = 0; i < proposalNames.length; i++) {
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
/// 只有chairperson有给toVoter地址投票的权利
function giveRightToVote(address voter) public {
require((msg.sender == chairperson) && ! voters[voter].voted && (voters[voter].weight == 0));
voters[voter].weight = 1;
}
/// 批量授予投票权
function giveRightToVoteByBatch(address[] batch) public {
require( msg.sender == chairperson );
for (uint i = 0; i < batch.length; i++) {
address voter = batch[i];
require( ! voters[voter].voted && (voters[voter].weight == 0) );
voters[voter].weight = 1;
}
}
/// 投票者将自己的投票机会授权另外一个地址
function delegate(address to) {
Voter storage sender = voters[msg.sender];
require((! sender.voted) && (sender.weight !=0 ));
require(to != msg.sender);
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
require(to != msg.sender);
}
sender.voted = true;
sender.delegate = to;
Voter storage delegate = voters[to];
if (delegate.voted) {
proposals[delegate.vote].voteCount += sender.weight;
} else {
delegate.weight += sender.weight;
}
}
/// 投票者根据提案编号proposal进行投票
function vote(uint proposal) {
require(proposal < proposals.length);
Voter storage sender = voters[msg.sender];
require((! sender.voted) && (sender.weight !=0 ));
sender.voted = true;
sender.vote = proposal;
proposals[proposal].voteCount += sender.weight;
}
/// 根据proposals里的票数统计voteCount计算出票数最多的提案编号
function winningProposal() constant returns(uint[] winningProposals) {
uint[] memory tempWinner = new uint[](proposals.length);
uint winningCount = 0;
uint winningVoteCount = 0;
for ( uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
tempWinner[0] = p;
winningCount = 1;
}else if (proposals[p].voteCount == winningVoteCount) {
tempWinner[winningCount] = p;
winningCount ++;
}
}
winningProposals = new uint[](winningCount);
for ( uint q = 0; q < winningCount; q++){
winningProposals[q] = tempWinner[q];
}
return winningProposals;
}
// 获取票数最多的提案名称。其中调用了winningProposal()函数
function winnerName() constant returns (bytes32[] winnerNames) {
uint[] memory winningProposals = winningProposal();
winnerNames = new bytes32[](winningProposals.length);
for (uint p = 0; p < winningProposals.length; p++){
winnerNames[p] = proposals[winningProposals[p]].name ;
}
return winnerNames;
}
}
然后就报错了。
开始调试
错误1:版本不兼容
原因:书中的代码是机遇solidity0.4.0写的,在0.5.0版本及以后,如果要指明当执行函数时不会去修改区块中的数据状态时(如只读),应当使用view关键字代替constant,view在0.4+版本里面与constant共存,官方解释是view的替代,constant在0.5.0版本中将会被去掉。依据这个,对代码中constant进行修改,并修改当前合约首行对版本的声明。
pragma solidity ^0.4.0; /// 改为 pragma solidity ^0.5.1; (当前最新版本)
还没结束,新的错误出现了,如下图所示,我们一个一个来。
版本问题-构造函数
原因:新的版本中,构造函数应当使用constructor(...) { ... }
声明,代码中定义的function BallotPro(bytes32[] proposalNames){...}
会被认为与合约名重名。
因此我们进行以下修改:
function BallotPro(bytes32[] proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1;
for (uint i = 0; i < proposalNames.length; i++) {
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
修改为
constructor (bytes32[] proposalNames){
chairperson = msg.sender;
voters[chairperson].weight = 1;
for (uint i = 0; i < proposalNames.length; i++) {
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
函数权限
原因:新版本中需要给函数显示设置权限。
扩展:solidity的函数权限
public类型的状态变量和函数的权限最大,可供外部、子合约、合约内部访问。
internal类型的状态变量可供外部和子合约调用。internal类型的函数和private类型的函数一样,智能合约自己内部调用,它和其他语言中的protected不完全一样。
private私有类型,修饰的状态变量及函数只能在当前合约内部使用,子合约也无法使用
因此,我们对代码中,需要进行修饰的函数添加public关键字,逐个修改。(小提示:public关键字需要在returns关键字之前声明)。
数据位置属性
原因:首先要理解在区块链里,区块链本身就是一个数据库。如果你使用区块链标记物产的所有权,
归属信息将会被记录到区块链上,所有人都无法篡改,以标明不可争议的拥有权。在solidity中,有一个数据位置的属性用来标识变量是否需要持久化到区块链中。其中storage修饰的变量是指永久存储在区块链中的变量,memory修饰变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。任何函数参数当它的类型为引用类型时,这个函数参数都默认为memory类型,在当前版本中我们需要指明变量的数据位置。因此我们需要做以下修改,对引用类型做参的提供memory关键字声明。
如:将function winningProposal() view public returns(uint[] winningProposals) {...}
修改为function winningProposal() view public returns(uint[] memory winningProposals){...}
,逐个修改。
至此,所以编译时错误都修改完毕,但还存在一个无法编译通过的warning。
声明重复
这个waring出现在下面代码中(书中代码不严谨):
function delegate(address to) public{
Voter storage sender = voters[msg.sender];
require((! sender.voted) && (sender.weight !=0 ));
require(to != msg.sender);
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
require(to != msg.sender);
}
sender.voted = true;
sender.delegate = to;
Voter storage delegate = voters[to];
if (delegate.voted) {
proposals[delegate.vote].voteCount += sender.weight;
} else {
delegate.weight += sender.weight;
}
}
原因:定义的delegate声明与当前方法名重复了,可以任意修改一处,比如将方法名修改为delegateTo
,改完你会发现,右侧编译工具栏,终于绿了(开心)。
虽然还存在一些warning,但不影响编译,主要是一些安全性、语法性建议,可以在工具栏的Analysis里面查看。
部署运行
代码逻辑
投票智能合约代码逻辑简单,与其他语言编写的逻辑一样,有点C++与JS结合的味道,语法上略有区别。因此逻辑看看代码都能明白,要去理解的是智能合约的机制。
按照当前代码的编写,合约初始化时,需要传入相关提案的数组(告知有哪些提案可以进行投票,为bytes32的数组)。这是我的测试数组(5个),可拿来使用,这就是每个提案在链中的代号:
["0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000003","0x0000000000000000000000000000000000000000000000000000000000000004"]
初始化过程中(对应当前合约的构造函数),现将合约部署者地址作为投票主持人地址,并且主持人默认成为参加投票的一份子,之后将传入的提案放进全局变量中,这样合约就部署完成了。
接下来按照代码的设计,合约部署者需要给给相关人(地址)进行投票权的赋予(赋权过程公开,不违背去中心化原则),对应代码中giveRightToVote
以及giveRightToVoteByBatch
方法,后者是批量授权,授权过程首先检测方法调用者是否为主持人(require( msg.sender == chairperson )
,require用来进行断定,条件为false时抛出异常),之后开始给每个地址赋权,如果传入的地址已投票过了,则不进行授权。
授权完成后,各地址可调用vote()
方法进行投票,传入体案的数组下标,即可完成。与赋权方法一样,每次执行前都会进行必要的检查,这也是智能合约开发的范式,检查-生效-交互。另外,还有一种投票方式,委托。委托就是我不想投了,你帮我来吧,但我会告诉你你可以代表我。地址A可以使用delegateTo()
方法,将自己的投票权赋予另一个地址B,那么B的意见就代表了A。如果出现链式委托,最终被委托者的意见代表前面所有委托者。
投票完成后,主持人开始唱票了,根据每个提案的投票人数,将得票最多的提案公布出来,对应winningProposal()
或winnerName()
方法。
运行测试
Remix提供了一系列工具帮助我们进行测试运行。
点击右侧工具栏的Run,可看到下列信息:
依次是:
Environment执行环境
Account账户(测试账户,默认含有100以太币)
Gas limit合约执行消耗的最大gas(可理解为执行成本)
Value 金额(单位,wei,以太币的单位,1 eth = 1e18 wei)
以上填写默认的就行。
接下来的下拉框是你编译成功的合约,选择需要部署的合约
下面有两个框框,第一个框框是合约的初始化数据,可通过初试化数据部署,第二个框框可通过合约地址部署,我们选择第一种。将上节举例用的bytes32数据组填入,点击Deploy。
在控制台能够看到,部署的中间信息,在后面的每一步执行过程只中,都可看到过程信息。
此时在右下侧的部署列表里,即可看到部署好的合约。其中红色按钮代表函数会改变变量,蓝色表示函数为只读。传入相关的正确的参数格式,可以开始玩了。
比如,我对下标为1的提案进行投票,之后查看获胜的提案:
必要说明
1.在remix开发环境中,每次部署,都会部署都会进行合约的发布(测试环境),那么部署多次(点击Deploy按钮多次)都是在发布新的合约,每个合约的发布者都是主持人,并不代表连上的不同节点,所以在这种情况下,委托、赋权方法都可执行成功,但并不会对部署多次的其他合约有影响,因此看不出此方法的成效,可以从控制台查看过程信息。(我还没找到生成多个节点调用一个合约的方法,不知道是不是在测试链不可用,只是为了调通合约,如果有知道的请指教)
2.对于solidity语言的特性,本文没有过多的说明,比如数组特性,存储特性,映射方法等等,篇幅不易过长。
3.虽说是调试过程,但更多的是像因对编程语言升级而做的调整,顺便熟悉一下remix编辑器。
4.remix还有很多功能,还在摸索中,除调试运行提供的工具外,他还可以将代码发布的gist上,发布成功了,发现gist还是在墙外…
周末愉快
完于 2019-04-21 16:06