1、区块链
不可篡改性
区块链的一个显著特点是,数据一旦写入链中,就不可篡改重写。
在传统的关系型数据库中,你可以很容易地更新一条数据记录。但是,在区块链中,一旦数据写入就无法 再更新了 —— 因此,区块链是一直增长的。
那么,区块链是如何实现数据的不可篡改特性?
这首先得益于哈希(Hash
)函数 —— 如果你还没接触过哈希函数,不妨将它视为一个数字指纹的计算函数: 输入任意长度的内容,输出定长的码流(指纹)。哈希函数的一个重要特性就是,输入的任何一点微小变化,都会 导致输出的改变。因此可以将哈希值作为内容的指纹来使用。 你可以点击这里 进一步了解哈希函数。
由于区块链里的每个块都存储有前一个块内容的哈希值,因此如果有任何块的内容被篡改,被篡改的块之后 所有块的哈希值也会随之改变,这样我们就很容易检测出区块链的各块是否被篡改了。
去中心化的挑战
所谓去中心化应用(DApp
:Dcentralized Application),就是一个不存在中心服务器 的应用。在网络中成百上千的电脑上,都可以运行该应用的副本,这使得它几乎不可能 出现宕机的情况。
基于区块链的投票是完全去中心化的,因此无须任何中心化机构的存在。
一旦完全去中心化,在网络上就会存在大量的区块链副本(即:全节点),很多事情都会变得比之前中心化 应用环境复杂的多,例如:
- 如何保证所有副本都已同步到最新状态?
- 如何保证所有交易都被广播到所有运行和维护区块链副本的节点计算机上?
- 如何防止恶意参与者篡改区块链
2、以太坊的去中心化应用架构
你应该已经注意到,每个客户端(浏览器)都是与各自的节点应用实例进行交互,而不是向 一个中心化的服务器请求服务。
在一个理想的去中心化环境中,每个想要跟DApp交互的人,都需要在他们的计算机或手机上面运行 一个的完整区块链节点 —— 简言之,每个人都运行一个全节点。这意味着,在能够真正使用一个 去中心化应用之前,用户不得不下载整个区块链。
不过我们并非生活在一个乌托邦里,期待每个用户都先运行一个全节点,然后再使用你的应用是不现实的。 但是去中心化背后的核心思想,就是不依赖于中心化的服务器。所以,区块链社区已经出现了 一些解决方案,例如提供公共区块链节点的Infura
, 以及浏览器插件Metamask
等。通过这些方案, 你就不需要花费大量的硬盘、内存和时间去下载并运行完整的区块链节点,同时也可以利用去中心化 的优点。我们将会以后的课程中对这些解决方案分别进行评测。
以太坊是一种区块链的实现。在以太坊网络中,众多的节点彼此连接,构成了以太坊网络:
以太坊节点软件提供两个核心功能:数据存储、合约代码执行。
在每个以太坊全节点中,都保存有完整的区块链数据。以太坊不仅将交易数据保存在链上,编译后 的合约代码同样也保存在链上。
以太坊全节点中,同时还提供了一个虚拟机来执行合约代码。
交易数据
以太坊中每笔交易都存储在区块链上。当你部署合约时,一次部署就是一笔交易。当你为候选者投票时,一次投票 又是另一笔交易。所有的这些交易都是公开的,每个人都可以看到并进行验证。这个数据永远也无法篡改。
为了确保网络中的所有节点都有着同一份数据拷贝,并且没有向数据库中写入任何无效数据,以太坊 目前使用工作量证明(POW:Proof Of Work
)算法来保证网络安全,即通过矿工挖矿(Mining
)来达成共识(Consensus
)—— 将数据同步到所有节点。
工作量证明不是达成共识的唯一算法,挖矿也不是区块链的唯一选择。现在,我们只需要了解,共识是指各节点 的数据实现了一致,POW
只是众多用于建立共识的算法中的一种,这种算法需要通过矿工的挖矿来实现非可信环境下的 可信交易。共识是目的,POW是手段。
合约代码
以太坊不仅仅在链上存储交易数据,它还可以在链上存储合约代码。
在数据库层面,区块链的作用就是存储交易数据。那么给候选者投票、或者检索投票结果的逻辑放在哪儿呢? 在以太坊的世界里,你可以使用Solidity
语言来编写业务逻辑/应用代码(也就是合约:Contract
), 然后将合约代码编译为以太坊字节码,并将字节码部署到区块链上:
编写合约代码也可以使用其他的语言,不过 Solidity
是到目前为止最流行的选择。
以太坊虚拟机
以太坊区块链不仅存储数据和代码,每个节点中还包含一个虚拟机(EVM:Ethereum Virtual Machine)来执行 合约代码 —— 听起来就像计算机操作系统。
事实上,这一点是以太坊区别于比特币(Bitcoin
)的最核心的一点:虚拟机的存在使区块链迈入了2.0 时代,也让区块链第一次成为应用开发者友好的平台。
JS开发库
为了便于构建基于web的DApp,以太坊还提供了一个非常方便的JavaScript库web3.js
,它封装了以太坊节点的API 协议,从而让开发者可以轻松地连接到区块链节点而不必编写繁琐的RPC
协议包。所以,我们可以在常用的JS框架 (比如 reactjs、angularjs 等)中直接引入该库来构建去中心化应用:
3、投票程序开发流程
整体构架
从图中可以看到,网页通过(HTTP上的)远程过程调用(RPC:Remote Procedure Call
)与区块链节点进行通信。web3.js
已经封装了以太坊规定的全部 RPC 调用,因此利用它就可以与区块链进行交互,而不必手写那些RPC请求包。 使用web3.js
的另一个好处是,你可以使用自己喜欢的前端框架来构建出色的web 应用。
由于获得一个同步的全节点相当耗时,并占用大量磁盘空间。为了在我们对区块链的兴趣消失之前掌握 如何开发一个去中心化应用,本课程将使用ganache
软件来模拟区块链节点,以便快速开发并测试应用, 从而可以将注意力集中在去中心化的思想理解与DApp应用逻辑开发方面。
接下来,我们将编写一个投票合约,然后编译合约并将其部署到区块链节点 —— ganache
上。
最后,我们将分别通过命令行和网页这两种方式,与区块链进行交互。
我们使用Solidity
语言来编写合约。如果你熟悉面向对象的开发和JavaScript
,那么学习Solidity
应该非常简单。可以将合约类比于OOP
的类:合约中的属性用来声明合约的状态,而合约中的方法则提 供修改状态的访问接口。下图给出了投票合约的主要接口:
基本上,投票合约Voting
包含以下内容:
- 构造函数,用来初始化候选人名单。
- 投票方法
Vote()
,每次执行就将指定的候选人得票数加 1 - 得票查询方法
totalVotesFor()
,执行后将返回指定候选人的得票数
有两点需要特别指出:
- 合约状态是持久化到区块链上的,因此对合约状态的修改需要消耗以太币。
- 只有在合约部署到区块链的时候,才会调用构造函数,并且只调用一次。
- 与 web 世界里每次部署代码都会覆盖旧代码不同,在区块链上部署的合约是不可改变的,也就是说,如果你更新 合约并再次部署,旧的合约仍然会在区块链上存在,并且合约的状态数据也依然存在。新的部署将会创建合约的一 个新的实例。
pragma solidity ^0.4.0; contract Voting { mapping (bytes32 => uint8) public votesReceived; bytes32[] public candidateList; function Voting(bytes32[] candidateNames) public { candidateList = candidateNames; } function totalVotesFor(bytes32 candidate) view public returns (uint8) { require(validCandidate(candidate)); return votesReceived[candidate]; } function voteForCandidate(bytes32 candidate) public { require(validCandidate(candidate)); votesReceived[candidate] += 1; } function validCandidate(bytes32 candidate) view public returns (bool) { for(uint i = 0; i < candidateList.length; i++) { if (candidateList[i] == candidate) { return true; } } return false; } }
编译器要求
pragma solidity ^0.4.18;
声明合约代码的编译器版本要求。^0.4.18
表示要求合约编译器版本不低于0.4.18
。
合约声明
contract Voting{}
contract
关键字用来声明一个合约。
字典类型:mapping
mapping (bytes32 => uint8) public votesReceived;
mapping
可以类比于一个关联数组或者是字典,是一个键值对。例如,votesReceived
状态的 键是候选者的名字,类型为bytes32
—— 32个字节定长字符串。votesReceived
状态中每个键对应的值 是一个单字节无符号整数(uint8
),用来存储该候选人的得票数:
bytes32[] public candidateList;
在JS中,使用votesReceived.keys
就可以获取所有的候选人姓名。但是在Solidity
中 没有这样的方法,所以我们需要单独管理全部候选人的名称 —— candidateList
数组。
function voteForCandidate(bytes32 candidate) public { require(validCandidate(candidate)); // 类似于if(!false) {return false} 的意思,只有为真,合约才继续执行。 votesReceived[candidate] += 1; }
在voteForCandidate()
方法中,请注意 votesReceived[key]
有默认值 0,所以我们没有进行初始化, 而是直接加1。
在合约方法体内的require()
语句类似于断言,只有条件为真时,合约才继续执行。validateCandidate()
方法只有在给定的候选人名称在部署合约时传入的候选人名单中时才返回真值,从而避免乱投票的行为:
function validCandidate(bytes32 candidate) view public returns (bool) { for(uint i = 0; i < candidateList.length; i++) { if (candidateList[i] == candidate) { return true; } } return false; }
方法声明符与修饰符
在Solidity
中,可以为函数应用可视性声明符(visibility specifier
),例如 public
、private
。 public
意味着可以从合约外调用函数。如果一个方法仅限合约内部调用,可以把它声明为私有(private
)。 点击这里 可以查看所有的可视性说明符。
在Solidity
中,还可以为函数声明修饰符(modifier
),例如view
用来告诉编译器,这个函数是只读的,也就是说, 该函数的执行不会改变区块链的状态)。所有的修饰符都可以在这里 看到。
合约代码编译
首先,请确保ganache
已经在第一个终端窗口中运行:~$ ganache-cli
。(对于ganache可运行 npm install -g ganache-cli 来全局安装 ganache )
然后,在另一个终端中进入repo/chapter1
目录,启动node 控制台,然后初始化 web3 对象,并向本地区块 链节点(http://localhost:8545
)查询获取所有的账户:
~$ cd ~/repo/chapter1 ~/repo/chapter1$ node > Web3 = require('web3') > web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); > web3.eth.accounts ['0x5c252a0c0475f9711b56ab160a1999729eccce97' '0x353d310bed379b2d1df3b727645e200997016ba3' '0xa3ddc09b5e49d654a43e161cae3f865261cabd23' '0xa8a188c6d97ec8cf905cc1dd1cd318e887249ec5' '0xc0aa5f8b79db71335dacc7cd116f357d7ecd2798' '0xda695959ff85f0581ca924e549567390a0034058' '0xd4ee63452555a87048dcfe2a039208d113323790' '0xc60c8a7b752d38e35e0359e25a2e0f6692b10d14' '0xba7ec95286334e8634e89760fab8d2ec1226bf42' '0x208e02303fe29be3698732e92ca32b88d80a2d36']
要编译合约,首先需要载入 Voting.sol
文件的内容,然后使用编译器(solc
)的compile()
方法 对合约代码进行编译:
> code = fs.readFileSync('Voting.sol').toString() > solc = require('solc') > compiledCode = solc.compile(code)
成功编译合约后可以查看一下编译结果。直接在控制台输入:
> compiledCode
相当长一大段输出霸屏...
便以结果是一个JSON对象,其中包含两个重要的字段:
- compiledCode.contracts[':Voting'].bytecode: 投票合约编译后的字节码,也是要部署到区块链上的代码。
- compiledCode.contracts[':Voting'].interface: 投票合约的接口,被称为应用二进制接口(
ABI:Application Binary Interface
), 它声明了合约中包含的接口方法。无论何时需要跟一个合约进行交互,都需要该合约的abi
定义。你可以在 这里查看ABI的详细信息。
关于代码:https://github.com/sunshineofparadise/solidity/tree/master/chapter1
原始代码存在冲突问题,所以可以直接拉取远程github代码
cd chapter1
npm安装
npm开始
/ *打开另一个终端窗口,确保两者都在运行。* /
节点
> Web3 = require(' web3 ')
> web3 = new Web3(new Web3.providers.HttpProvider(“ http:// localhost:8545 ”));
> web3.eth.accounts
> code = fs.readFileSync('Voting.sol',‘utf8’)。toString()//这两行的改动是为了防止报错,可不该,报错了再运行这个
> solc = require(' solc ',1)
> compiledCode = solc.compile(code)
将投票合约部署到区块链上
需要先传入合约的abi
定义来创建合约对象VotingContract
,然后利用该对象完成合约在链上的部署和初始化。
在node控制台执行以下命令:
> abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface) > VotingContract = web3.eth.contract(abiDefinition) > byteCode = compiledCode.contracts[':Voting'].bytecode > deployedContract = VotingContract.new(['Rama','Nick','Jose'],{data: byteCode, from: web3.eth.accounts[0], gas: 4700000}) > deployedContract.address '0x0396d2b97871144f75ba9a9c8ae12bf6c019f610' <- 你的部署地址可能和这个不一样 > contractInstance = VotingContract.at(deployedContract.address)
调用VotingContract
对象的new()
方法来将投票合约部署到区块链。new()
方法参数列表应当与合约的 构造函数要求相一致。对于投票合约而言,new()
方法的第一个参数是候选人名单。
new()
方法的最后一个参数用来声明部署选项。现在让我们来看一下这个参数的内容:
{ data: byteCode, //合约字节码 from: web3.eth.accounts[0], //部署者账户,将从这个账户扣除执行部署交易的开销 gas: 4700000 //愿意为本次部署最多支付多少油费,单位:Wei }
- data: 这是合约编译后,需要部署到区块链上的合约字节码。
- from: 区块链必须跟踪是谁部署了一个合约。在本例中,我们简单地利用
web3.eth.accounts
返回的 第一个账户,作为部署这个合约的账户。在提交交易之前,你必须拥有并解锁这个账户。不过为了方便 起见,ganache
默认会自动解锁这10个账户。 - gas: 与区块链进行交互需要消耗资金。这笔钱用来付给矿工,因为他们帮你把代码部署到在区块链里。你 必须声明愿意花费多少资金让你的代码包含在区块链中,也就是设定 “gas” 的值。“from”字段声明的账户的 余额将会被用来购买 gas。gas 的价格则由区块链网络设定。
我们已经成功部署了投票合约,并且获得了一个合约实例(变量contractInstance
),现在可以用这个实例 与合约进行交互了。
在区块链上有上千个合约。那么,如何识别你的合约已经上链了呢?
答案是:使用deployedContract.address
。 当你需要跟合约进行交互时,就需要这个部署地址和我们之前 谈到的abi
定义。 因此,请记住这个地址。
调用合约的totalVotesFor()
方法来查看某个候选人的得票数。例如,下面的代码 查看候选人Rama
的得票数:
> contractInstance.totalVotesFor.call('Rama') { [String: '0'] s: 1, e: 0, c: [ 0 ] }
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
是数字 0 的科学计数法表示. 你可以在 这里 了解科学计数法的详细信息。
调用合约的voteForCandidate()
方法投票给某个候选人。下面的代码给Rama
投了三次票:
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]}) '0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53' > contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]}) '0x02c054d238038d68b65d55770fabfca592a5cf6590229ab91bbe7cd72da46de9' > contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]}) '0x3da069a09577514f2baaa11bc3015a16edf26aad28dffbcd126bde2e71f2b76f'
现在我们再次查看Rama
的得票数:
> contractInstance.totalVotesFor.call('Rama').toLocaleString() '3'
投票 = 交易
每执行一次投票,就会产生一次交易,因此voteForCandidate()
方法将返回一个交易id,作为 交易的凭据。比如:0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53
。 交易id是交易发生的凭据,交易是不可篡改的,因此任何时候可以使用交易id引用或查看交易内容 都会得到同样的结果。
对于区块链而言,交易不可篡改是其核心特性。接下来,我们将会利用这一特性来构建应用。
接下来让我们创建一个简单的html
页面,以便用户可以使用浏览器 而不是复杂的命令行来与投票合约交互:
页面的主要功能如下:
- 列出所有的候选人及其得票数
- 用户在页面中可以输入候选人的名称,然后点击投票按钮,网页中的JS代码将调用投票合约的
voteForCandidate()
方法 —— 和我们nodejs控制台里的流程一样。
你可以在实验环境编辑器中打开~/repo/chapter1/index.html
来查看页面源代码。 为了聚焦核心业务逻辑,我们在网页中硬编码了候选人姓名。如果你喜欢的话,可以调整代码来动态生成候选人。
页面文件中的JS代码都封装在了一个单独的JS文件中,可以在试验环境编辑器中打开 ~/repo/chapter1/index.js
来查看其内容。
为了将页面运行起来,需要根据你的私有试验环境对JS代码进行一下调整:
节点的RPC API地址:
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
HttpProvier()
对象的构造函数参数是web3js库需要链接的以太坊节点RPC API的URL,要调整为 你的私有试验环境中ganache
的访问端结点,格式为:
http://8545.<你的私有实验环境URL>/
查看试验环境中的嵌入浏览器地址栏来获取你的私有实验环境URL:
投票合约地址
当一个合约部署到区块链上时,将获得一个地址,例如0x329f5c190380ebcf640a90d06eb1db2d68503a53
。 由于每次部署都会获得一个不同的地址,因此你需要指定它:
contractInstance = VotingContract.at('0x329f5c190380ebcf640a90d06eb1db2d68503a53')
如果你在部署合约的时候没有记录这个地址,就重新部署吧。
运行web服务
在第二个终端中输入以下命令来启动一个简单的Web服务器,以便我们可以在试验环境中的嵌入浏览器中访问页面:
~$ cd ~/repo/chapter1
~/repo/chapter1$ python -m SimpleHTTPServer
Python的SimpleHTTPServer
模块将启动在8000端口的监听。
现在,在试验环境的嵌入浏览器中点击刷新按钮。如果一切顺利的话,你应该可以看到投票应用的页面了。 当你在文本框中输入候选人姓名,例如Rama
,然后点击按钮后,应该会看到候选人Rama
的得票数加 1 。
注:其中纯nodejs开发项目可参考项目
4、使用Truffle投票程序
Truffle
是一个DApp开发框架,它简化了去中心化应用的构建和管理。你可以在 这里了解框架的 更多内容和完整特性。
如果你需要在自己的机器上安装,可以使用 npm 全局安装:npm install -g truffle
end