[solidity语法学习一]基于Remix以太坊的voting模板代码分析

前言

编译环境:Remix.
代码来源:SolidityDoc.
疑难解决来源:Ethereum gitter.
为了能够更加深入了解solidity语言应用,我计划把solidityDoc里面的三个实用例子:Voting,Blind Auction,and Micropayment Channel。进行分析。并结合我自身遇到的问题以及解决方案进行提出,从而减少后来学习者的学习成本。
接下来我将就voting代码进行分析,并在文章的最后对遇到的问题逐一分析并且解答。如果有什么疑问欢迎在评论区一同探讨。

代码分析

一、定义函数所需的变量

    struct Voter {
        uint weight; //票的价值,默认为1,如果出现被委托会增加价值
        bool voted;  // 判断bool型变量:是否投过票
        address delegate; // 委托账户的地址
        uint vote;   // 表示候选人的数组序号
    }

    struct Proposal {
        // 可以通过选择合适的数据类型如bytes1~bytes32从而对名称的长度进行限制
        bytes32 name;   // 短名称,长度取决于数据类型。
        //bytes4要求输入0x00000000 即8位16进制的数据
        //若选用bytes32 则要输入0x0(总共64位的数据),这也为接下来constructor函数输入合适的数据类型做下铺垫
        //结合上述类推对应的输入数据
        uint voteCount; // 收到的投票总数
    }
    address public chairperson; //定义一个负责人的地址用于分配投票权利
    mapping(address => Voter) public voters;
    //为每个外部地址分配一个Voter结构体映射
    Proposal[] public proposals;//建议一个候选人数组proposals

二、设置构造函数constructor(即在合约创建之前所执行的函数)

    constructor(bytes32[] memory proposalNames) public {
        chairperson = msg.sender;//令合约地址为负责人
        voters[chairperson].weight = 1;//令合约地址票的价值为1

        for (uint i = 0; i < proposalNames.length; i++) {
            // 根据输入候选人名称的长度,依次添加proposal数组的Proposal结构体
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

我来分析一下构造函数的用法。

首先,constructor函数主要用于设置属于创建合约的地址所赋予的权限。比如在这里用(chairperson = msg.sender)令负责人为创造合约的地址,从而通过require(msg.sender == chairperson,“Only chairperson can give right to vote”)判断来实现负责人对其他外部账户进行赋权控制。

其次,可以在合约工作之前设定一些不可改变的参数。比如该代码主要是用于设计候选人名称所构成的数组。合约账户通过设定符合条件的输入从而确定整个合约账户围绕着具体的数据进行工作,这里就是围绕着候选人名称以及获得票总数进行工作。

最后,在未来肯定有更加高级的用法。这里就不再具体阐述。

这里补充一个自己在摸索代码中遇到的问题:
Q1:第一次遇到需要输入数据的部署,导致完全不知道怎么办?
在这里插入图片描述
A1:以上问题产生原因在于,输入一个不符合数据类型的数据所报错。
首先得捋清几个概念:
1.address:可以是Remix上不同ACCOUNT.在接下来调试过程中要充分利用不同的地址进行投票,赋权,委任等等。
2.bytes32[] :由byte32格式的数据构成。byte32的数据类型为"0x3100000000000000000000000000000000000000000000000000000000000000" (就是带着双引号,中间为0xX,X一共有32*2=64个数字)考虑到这是最长的宽度。因此在我们实际运用过程中,可以把bytes32[] 改成bytes2[]或者bytes4[]。毕竟在测试过程中所需要的样本不是特别打,因此只要bytes2[]输入(0x0000~0xFFFF) 或者bytes4[]输入(0x00000000 ~ 0xFFFFFFFF)等数据即可。

PS:这里如果按照模版文件也就是bytes32[]的情况下,这边默认输入的格式为:[“0x3100000000000000000000000000000000000000000000000000000000000000”,“0x3100000000000000000000000000000000000000000000000000000000000001”]

千万不要忘记外面两个中括号,代表的是输入是一个数组格式。

Q2:在这段例程代码中引入一种结构体赋值方式以及数组依次添加。
A2:
数组.push(结构体({name:proposalNames[i],voteCount:0}))
具体的实现方式理解如下:

//1.首先定义一个结构体:
 struct Proposal {
        bytes32 name;  
        uint voteCount; 
    }
//2.以结构体的数据类型定义一个结构体数组
Proposal[] public proposals;
//3.由byte32[] memory proposalNames输入数据并存储在内存中
constructor(bytes32[] memory proposalNames) public {}
//4.用for循环依次把内存中的proposalNames里的名字添加到候选人数组:proposals
for (uint i = 0; i < proposalNames.length; i++) {
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
//关键在于proposals.psuh表明在原有数组proposals后面添加新的数组(原有数组为0)。
//Proposal({name:proposalName[i],voteCount:0})主要是明确在Proposal结构体下,name参数:赋什么值,voteCount参数:赋什么值等等

三、功能函数:赋予权力

  function giveRightToVote(address voter) public {
        require(
            msg.sender == chairperson,
            "Only chairperson can give right to vote."
        );//判断是否合约账户(负责人)调用该赋权函数
        require(
            !voters[voter].voted,
            "The voter already voted."
        );//判断赋权地址是否投过票
        require(voters[voter].weight == 0);//判断是否赋权过
        voters[voter].weight = 1;
    }

在测试用例的最后提出来这样一个问题:
Currently, many transactions are needed to assign the rights to vote to all participants. Can you think of a better way?
意思是当前需要进行多笔交易从而把投票权分配给所有的参与者,请问你有更好的方法吗?

在这里我的第一反应是想着像其他编程语言一样,在构造结构体的同时默认对weight = 1 进行赋值。但是在solidity语言里面无法这样做,因此我选择了重新写一个函数,这个函数不需要经过负责任进行赋值,同样的每个参与者都可以主动调用函数。
该函数会判断该地址是否投过票以及判断该地址的价值是否为0,从而进行赋值。由于该合约可以被任何外部账户调入,因此可以保证任何一个参与者都可以主动获得投票权。但是存在隐患就是无法对投票者进行限制。

    function getRight() public{
        require(!voters[msg.sender].voted, "The voter already voted.");
        require(voters[msg.sender].weight == 0);
        voters[msg.sender].weight = 1;
    }

四、功能函数:委托

    function delegate(address to) public {
    	//设置结构体指针sender指向voters[msg.sender]
        Voter storage sender = voters[msg.sender];
       	//判断是否投票过
        require(!sender.voted, "You already voted.");
 		//判断是否委托自己,出现自我委托错误。
        require(to != msg.sender, "Self-delegation is disallowed.");
		//递归遍历找到最终委托方,因为最终委托方的委托地址为0从而停止循环
        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;
            // 若出现一个循环递归,也就是递归到自己本身会发生错误,从而复原所有修改过的数据并结束函数
            require(to != msg.sender, "Found loop in delegation.");
        }
        sender.voted = true;//由于委托过投票,所以该地址的voted有效值为true
        sender.delegate = to;//最终委托地址赋值给结构体里的delegate
        Voter storage delegate_ = voters[to];
        //定义一个结构体指针delegate_指向最终委托地址的结构体
        if (delegate_.voted) {
            // 如果已经投过票,令候选人结构体数组对应编号的voteCount+sender.weight
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            // 如果没有投票,则委托的投票价值+sender.weight
            delegate_.weight += sender.weight;
        }
    }

在实际运用过程中,我发现这个程序存在这么一个隐患。一个没有投票权的账户也可以正常委派另外一个地址进行投票,虽然不会增加委派的weight或者候选人的voteCount,但是假如后面负责人赋予该地址投票权会存在已经投过票的情况。
因此我认为应该在委派函数中增加一条require(weight!=0);的判断

五、功能函数:投票

    function vote(uint proposal) public {
    	//设置结构体指针sender指向外部账户对应的结构体
        Voter storage sender = voters[msg.sender];
      	//判断是否具有投票权,关键是send.weight是否为0
        require(sender.weight != 0, "Has no right to vote");
        //判断是否bool型变量是否为false,即是否投过票
        require(!sender.voted, "Already voted.");
        //令voted为true
        sender.voted = true;
        //令vote的变量为候选人的数组编号
        sender.vote = proposal;

        //当然如果输入的proposal的序号溢出,也就是≥现有编号,会出现报错。具体见下图 
        proposals[proposal].voteCount += sender.weight;//对候选人结构体的voteCount进行相加
    }

候选人有0、1两个编号,在这里我调用vote函数,然后输入的proposal为5,于是控制台返回的错误信息如下。 invalid opcode(无效的操作吗)
[solidity语法学习一]基于Remix以太坊的voting模板代码分析_第1张图片

六、功能函数:赢家名称以及赢家数组序号

//显示赢家的候选数组编号
 function winningProposal() public view
            returns (uint winningProposal_)
    {
    	//定义一个局部计分变量
        uint winningVoteCount = 0;
        //对整个候选数组进行循环,依次比较从而选出候选数组的序号,并将序号赋值给winningProposal_ 返回出来。
        //因此可以通过日志观察到output的数据
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }
//直接调用上述候选数组编号,从候选数组中查候选人名称,并赋值给winnerName_进行返回
function winnerName() public view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }

这里补充一个solidityDoc里面的小知识点:
其实:
address public minter;

function minter() external view returns (address) { return minter; }
是同一个意思。相当于如果我们通过定义一个public条件下,address数据类型的minter ,等价于
定义一个函数:
function minter() external view returns(address) {return minter;}

因此,如果我们在定义变量的同时加入一个public,我们就可以在remix平台下看到查看指定变量的值的按钮如:
[solidity语法学习一]基于Remix以太坊的voting模板代码分析_第2张图片
综上所述:像下面两个带返回值的函数需要通过一系列逻辑计算的时候,我们可以定义成一个返回值函数。如果想通过外部地址直接查看变量中所含的值,请务必在定义数据时加入一个public的前缀。

总结以及感悟

我个人认为这个投票函数还存在一部分逻辑漏洞。
一、比如只要每个地址访问该智能合约,都会成为一个不具有投票权的参与者。如果以chairperson对参与者依次赋权的话,当参与人数过多的时候会极大地增加交易量。也就是doc里面提出的问题,如何更好地赋权。

现在的自己对整个solidity的理解还不够深入,因此暂时还没有找到可参考的代码用于解决这个问题,希望在接下来的学习中能够解决这个问题,并争取回来对这里进行解答。

最后在写这篇博客的同时又让我意识到几个问题并尝试去解决,从而加深了我对相关逻辑的理解。由于能力有限,现在还只能理解到这里。希望在日后的学习中能够不断发现新问题,掌握新知识,从而提升自己在solidity语法这方面的能力。

你可能感兴趣的:(区块链,编程语言,智能合约,以太坊,经验分享)