记录一次以太坊投票智能合约的调试过程

在看到这个代码以及编译部署方式后,就想着来执行下,但路途坎坷.

根据书上的说法,我把代码放进IDE,点这再点那,就可以了,但事实并没有这么简单。

记录下坎坷的过程。

打开IDE

Remix是官方推荐的智能合约开发IDE,适合新手,可以在浏览器中快速部署测试智能合约。打开这个网址就行(https://remix.ethereum.org)。

打开IDE

点击左上角小加号,添加名为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。

在控制台能够看到,部署的中间信息,在后面的每一步执行过程只中,都可看到过程信息。

debug

此时在右下侧的部署列表里,即可看到部署好的合约。其中红色按钮代表函数会改变变量,蓝色表示函数为只读。传入相关的正确的参数格式,可以开始玩了。

部署列表

比如,我对下标为1的提案进行投票,之后查看获胜的提案:

函数执行

必要说明

1.在remix开发环境中,每次部署,都会部署都会进行合约的发布(测试环境),那么部署多次(点击Deploy按钮多次)都是在发布新的合约,每个合约的发布者都是主持人,并不代表连上的不同节点,所以在这种情况下,委托、赋权方法都可执行成功,但并不会对部署多次的其他合约有影响,因此看不出此方法的成效,可以从控制台查看过程信息。(我还没找到生成多个节点调用一个合约的方法,不知道是不是在测试链不可用,只是为了调通合约,如果有知道的请指教)

2.对于solidity语言的特性,本文没有过多的说明,比如数组特性,存储特性,映射方法等等,篇幅不易过长。

3.虽说是调试过程,但更多的是像因对编程语言升级而做的调整,顺便熟悉一下remix编辑器。

4.remix还有很多功能,还在摸索中,除调试运行提供的工具外,他还可以将代码发布的gist上,发布成功了,发现gist还是在墙外…

周末愉快


完于 2019-04-21 16:06

Licensed under CC BY-NC-SA 4.0
本文链接:http://JohnneyAnn.github.io/2019/04/21/记录一次以太坊投票智能合约的调试过程/