专栏名称: 沪江技术学院
开发
目录
相关文章推荐
51好读  ›  专栏  ›  沪江技术学院

DAPP开发实战之投票系统(上)

沪江技术学院  · 掘金  ·  · 2018-04-11 07:47

正文

文: Xwang

本文原创,转载请注明作者及出处

区块链技术其实大概已经出来很长一段时间了,只是在去年随着各种ico着实是火了一把。如同很多刚出现的技术与概念一样,信仰者奉之为神,觉得甚至是可以在短时间内颠覆支付宝的东西;而不信的人又是嗤之以鼻,认为不过都是些骗人的老套路套着新概念又重出江湖罢了。这是最好的时代,也是最坏的时代,人人都可能相比于几十年前不用那么勤奋努力很多很多年才能获得足够的财富,而就是因为人人都有机会,才让竞争也变得激烈异常。

作为技术研究者一般都信仰技术无罪,本文也无意谈太多技术之外的八卦,文章的技术核心内容基本都是来自于网上加以整理和实践,没有比模仿更适合了解和掌握一门新技术了。

一.项目框架概述

开门见山地,上图反应出一种互不信任的,去中心化的投票系统架构,和其他去中心化系统一样,它并没有一个中央服务器来保存数据,每个运行的节点(用户运行的客户端)都保留一份完整的全链路数据,区块链不断增长且不可篡改,每个节点都完全平等。每次数据的变动都是客户端与各自节点实例的交互,然后广播全网络同步的过程,如下图

当然,这是一种比较理想的情况,在移动互联网时代这可能是最急迫需要解决的问题,相信也很少有人能够忍受在手机上运行DAPP像在PC上打开以太坊全量钱包那样可能等上个一整天去同步所有区块数据。区块链社区也已经出现了一些解决方案,例如提供公共区块链节点的 Infura , 以及浏览器插件 Metamask 等。通过这些方案,就不需要花费大量的硬盘、内存和时间去下载并运行完整的区块链节点,同时也可以利用去中心化的优点。当然这并不是本文所要讨论的重点。

以下是本次应用的架构图

从图中可以看到,网页通过(HTTP上的)远程过程调用(RPC:Remote Procedure Call)与区块链节点进行通信。 web3.js 已经封装了以太坊规定的全部RPC调用,因此利用它就可以与区块链进行交互,而不必手写那些RPC请求包。使用 web3.js 的另一个好处是,我们可以使用自己喜欢或擅长的前端框架来构建出色的web应用。

由于获得一个同步的全节点相当耗时,并占用大量磁盘空间。为了能快速地掌握如何开发一个去中心化应用,本文将使用 ganache 软件来模拟区块链节点,以便快速开发并测试应用,从而可以将注意力集中在去中心化的思想理解与DApp应用逻辑开发方面。

接下来,我们先将编写一个投票合约,然后编译合约并将其部署到区块链节点 —— ganache 上。

最后,我们将分别通过命令行和网页这两种方式,与区块链进行交互。

二.使用Node.js进行第一次迭代

2.1

由于各自环境的不同,这里以MAC为准,其他平台可以自行用谷歌百度一下。

由上文可知,首先是安装 gannache 使用命令 sudo npm install-g ganache-cli 关于 Ganache 可以百度先了解一下

Ganache:以前叫作 TestRPC,它在 TestRPC 和 Truffle 的集成后被重新命名为 Ganache。Ganache 的工作很简单:创建一个虚拟的以太坊区块链,并生成一些我们将在开发过程中用到的虚拟账号。

安装完毕后在控制台输入 ganache-cli 命令,可以看到以下结果

这样就相当于一个私链已经存在了,并且 ganache 默认创建了10个测试账号,每个账号里面也会有一些余额。

当然还有一个GUI版本的,有兴趣也可以用这个 http://truffleframework.com/ganache/#2/_blank

以太坊开发目前都是使用 Solidity 语言进行开发,个人感觉如果有面向对象基础的话就不要从最最基本的语法看起,可能还没入门就直接想放弃了,这也是为什么要鼓励直接实战的原因。下图是投票合约的主要接口

基本上,投票合约Voting包含以下内容:

  • 构造函数,用来初始化候选人名单。

  • 投票方法 Vote() ,每次执行就将指定的候选人得票数加 1

  • 获得票数查询方法 totalVotesFor() ,执行后将返回指定候选人的得票数

有几点需要注意一下:

  • 合约状态是持久化到区块链上的,因此对合约状态的修改需要消耗以太币。

  • 只有在合约部署到区块链的时候,才会调用构造函数,并且只调用一次。

  • 与 web 世界里每次部署代码都会覆盖旧代码不同,在区块链上部署的合约是不可改变的,也就是说,如果你更新合约并再次部署,旧的合约仍然会在区块链上存在,并且合约的状态数据也依然存在。新的部署将会创建合约的一个新的实例。

2.2

下面直接看看投票合约代码

  1. pragma solidity ^0.4.18;

  2. contract Voting {

  3.  mapping (bytes32 => uint8) public votesReceived;

  4.  bytes32[] public candidateList;

  5.  function Voting(bytes32[] candidateNames) public {

  6.    candidateList = candidateNames;

  7.  }

  8.  function totalVotesFor(bytes32 candidate) view public returns (uint8) {

  9.    require(validCandidate(candidate));

  10.    return votesReceived[candidate];

  11.  }

  12.  function voteForCandidate(bytes32 candidate) public {

  13.    require(validCandidate(candidate));

  14.    votesReceived[candidate]  += 1;

  15.  }

  16.  function validCandidate(bytes32 candidate) view public returns (bool) {

  17.    for(uint i = 0; i < candidateList.length; i++) {

  18.      if (candidateList[i] == candidate) {

  19.        return true;

  20.      }

  21.    }

  22.    return false;

  23.   }

  24. }

代码不长,大概有下面几个点

  • 编译器版本声明

  • 合约类声明,构造函数

  • 字典,数组

  • 函数,断言

稍微看看应该就基本都能看懂。

我们使用 solc 库来编译合约代码。然后使用 web3js 库,它能够让你通过RPC与区块链进行交互。我们将在node控制台里用这个库编译和部署合约,并与区块链进行交互。 首先,请确保ganache已经在第一个终端窗口中运行: ~$ ganache-cli

  1. $ node

  2. > Web3 = require('web3')

  3. > web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));

  4. > web3.eth.accounts

要编译合约,首先需要载入 Voting.sol 文件的内容,然后使用编译器(solc)的compile()方法对合约代码进行编译:

  1. > code = fs.readFileSync('Voting.sol').toString()

  2. > solc = require('solc')

  3. > compiledCode = solc.compile(code)

以上只要没报错就OK,尤其是 compiledCode ,会出现特别多的输出信息。 其中包含两个重要的字段:

  • compiledCode.contracts[':Voting'].bytecode : 投票合约编译后的字节码,也是要部署到区块链上的代码。

  • compiledCode.contracts[':Voting'].interface : 投票合约的接口,被称为应用二进制接口(ABI:Application Binary Interface),它声明了合约中包含的接口方法。无论何时需要跟一个合约进行交互,都需要该合约的abi定义。

2.3

合约编译基本已完成,接下来看看怎么部署到区块链上去。 为此,需要先传入合约的abi定义来创建合约对象 VotingContract ,然后利用该对象完成合约在链上的部署和初始化。 命令如下

  1. > abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface)

  2. > VotingContract = web3.eth.contract(abiDefinition)

  3. > byteCode = compiledCode.contracts[':Voting'].bytecode

  4. > deployedContract = VotingContract.new(['Rama','Nick','Jose'],{data: byteCode, from: web3.eth.accounts[0], gas: 4700000})

  5. > deployedContract.address

  6. > contractInstance = VotingContract.at(deployedContract.address)

调用 VotingContract 对象的 new() 方法来将投票合约部署到区块链。 new() 方法参数列表应当与合约的构造函数要求相一致。对于投票合约而言, new() 方法的第一个参数是候选人名单。

new() 方法的最后一个参数用来声明部署选项。先来看一下这个参数的内容:

  1. {

  2.  data: byteCode,             //合约字节码

  3.  from: web3.eth.accounts[0], //部署者账户,将从这个账户扣除执行部署交易的开销

  4.  gas: 4700000                //愿意为本次部署最多支付多少油费,单位:Wei

  5. }

  • data: 这是合约编译后,需要部署到区块链上的合约字节码。

  • from: 区块链必须跟踪是谁部署了一个合约。在本例中,我们简单地利用 web3.eth.accounts 返回的第一个账户,作为部署这个合约的账户。在提交交易之前,你必须拥有并解锁这个账户。不过为了方便起见,ganache默认会自动解锁这10个账户。

  • gas: 与区块链进行交互需要消耗资金。这笔钱用来付给矿工,因为他们帮你把代码部署到在区块链里。你必须声明愿意花费多少资金让你的代码包含在区块链中,也就是设定 gas 的值。 from 字段声明的账户的余额将会被用来购买 gas gas 的价格则由区块链网络设定。

2.4

拿到 contractInstance 实例之后可以根据合约定义的接口进行一系列的操作了。

调用合约的 totalVotesFor() 方法来查看某个候选人的得票数。 例如,下面的代码查看候选人Rama的得票数: contractInstance.totalVotesFor.call('Rama')

  1. { [String: '0'] s: 1, e: 0, c: [ 0 ] }

  2. 是数字 0 的科学计数法表示.

调用合约的 voteForCandidate() 方法投票给某个候选人。下面的代码就是给Rama投了三次票:

>contractInstance.voteForCandidate('Rama',{from:web3.eth.accounts[0]}) >contractInstance.voteForCandidate('Rama',{from:web3.eth.accounts[0]}) >contractInstance.voteForCandidate('Rama',{from:web3.eth.accounts[0]})

现在我们再次查看Rama的得票数: >contractInstance.totalVotesFor.call('Rama').toLocaleString()

  1. '3'

每执行一次投票,就会产生一次交易,因此 voteForCandidate() 方法将返回一个交易id,作为交易的凭据。比如: 0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53 。交易id是交易发生的凭据,交易是不可篡改的,因此任何时候可以使用交易id引用或查看交易内容都会得到同样的结果。对于区块链而言, 交易不可篡改 是其核心特性。

2.5

让用户使用命令行显然是非常不友好的,所以接下来尝试使用网页来作为前端交互页面。

页面的主要功能如下:

  • 列出所有的候选人及其得票数

  • 用户在页面中可以输入候选人的名称,然后点击投票按钮,网页中的JS代码将调用投票合约的 voteForCandidate() 方法 —— 和我们nodejs控制台里的流程一样。

HTML代码里面引用了一些基本的JS库,此外还有 web3.js index.js ,为了方便跑代码把这篇文章的相关代码都传上了git,可以需者自取。

其他的js文件包括 web3.js 都是库文件,没有业务逻辑,而需要我们关注的是 index.js 这个文件,也是我们自己写的js文件。

可以在文章后面找到代码存放地址。

为了将页面运行起来,需要对JS代码进行一下调整: 节点的RPC API地址 web3=newWeb3(newWeb3.providers.HttpProvider("http://localhost:8545")); HttpProvier()对象的构造函数参数是 web3js 库需要链接的以太坊节点RPC API的URL。

当一个合约部署到区块链上时,将获得一个地址,例如 0x329f5c190380ebcf640a90d06eb1db2d68503a53。

由于每次部署都会获得一个不同的地址,因此你需要指定它: contractInstance=VotingContract.at('0x329f5c190380ebcf640a90d06eb1db2d68503a 53')

在第二个终端中输入以下命令来启动一个简单的Web服务器,以便我们可以在浏览器中访问页面:

  1. ~$ cd ~/repo/chapter1

  2. ~/repo/chapter1$ python -m SimpleHTTPServer

  3. (python3使用这个,下同) python -m http.server 80

Python的 SimpleHTTPServer 模块将启动在8000端口的监听。 现在,在浏览器中点击刷新按钮。如果一切顺利的话,你应该可以看到投票应用的页面了。 当你在文本框中输入候选人姓名,例如Rama,然后点击按钮后,应该会看到候选人Rama的得票数加 1 。

2.6

如果能看到页面,并能够正常投票,第一个基本demo已经能跑起来了。

总结一下,下面是我们到目前为止已经完成的事情:

  • 使用 nodejs , npm ganache 作为开发环境。

  • 开发简单的投票合约,编译并部署到区块链节点上。

  • 使用 nodejs 控制台与合约交互。

  • 编写网页与合约交互。

  • 所有的投票都保存到区块链上,并且不可修改。







请到「今天看啥」查看全文