不可篡改性javascript
区块链的一个显著特色是,数据一旦写入链中,就不可篡改重写。php
在传统的关系型数据库中,你能够很容易地更新一条数据记录。可是,在区块链中,一旦数据写入就没法 再更新了 —— 所以,区块链是一直增加的。css
那么,区块链是如何实现数据的不可篡改特性?html
这首先得益于哈希(Hash
)函数 —— 若是你还没接触过哈希函数,不妨将它视为一个数字指纹的计算函数: 输入任意长度的内容,输出定长的码流(指纹)。哈希函数的一个重要特性就是,输入的任何一点微小变化,都会 致使输出的改变。所以能够将哈希值做为内容的指纹来使用。 你能够点击这里 进一步了解哈希函数。前端
因为区块链里的每一个块都存储有前一个块内容的哈希值,所以若是有任何块的内容被篡改,被篡改的块以后 全部块的哈希值也会随之改变,这样咱们就很容易检测出区块链的各块是否被篡改了。java
去中心化的挑战node
所谓去中心化应用(DApp
:Dcentralized Application),就是一个不存在中心服务器 的应用。在网络中成百上千的电脑上,均可以运行该应用的副本,这使得它几乎不可能 出现宕机的状况。python
基于区块链的投票是彻底去中心化的,所以无须任何中心化机构的存在。react
一旦彻底去中心化,在网络上就会存在大量的区块链副本(即:全节点),不少事情都会变得比以前中心化 应用环境复杂的多,例如:git
你应该已经注意到,每一个客户端(浏览器)都是与各自的节点应用实例进行交互,而不是向 一个中心化的服务器请求服务。
在一个理想的去中心化环境中,每一个想要跟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 等)中直接引入该库来构建去中心化应用:
总体构架
从图中能够看到,网页经过(HTTP上的)远程过程调用(RPC:Remote Procedure Call
)与区块链节点进行通讯。web3.js
已经封装了以太坊规定的所有 RPC 调用,所以利用它就能够与区块链进行交互,而没必要手写那些RPC请求包。 使用web3.js
的另外一个好处是,你可使用本身喜欢的前端框架来构建出色的web 应用。
因为得到一个同步的全节点至关耗时,并占用大量磁盘空间。为了在咱们对区块链的兴趣消失以前掌握 如何开发一个去中心化应用,本课程将使用ganache
软件来模拟区块链节点,以便快速开发并测试应用, 从而能够将注意力集中在去中心化的思想理解与DApp应用逻辑开发方面。
接下来,咱们将编写一个投票合约,而后编译合约并将其部署到区块链节点 —— ganache
上。
最后,咱们将分别经过命令行和网页这两种方式,与区块链进行交互。
咱们使用Solidity
语言来编写合约。若是你熟悉面向对象的开发和JavaScript
,那么学习Solidity
应该很是简单。能够将合约类比于OOP
的类:合约中的属性用来声明合约的状态,而合约中的方法则提 供修改状态的访问接口。下图给出了投票合约的主要接口:
基本上,投票合约Voting
包含如下内容:
Vote()
,每次执行就将指定的候选人得票数加 1totalVotesFor()
,执行后将返回指定候选人的得票数有两点须要特别指出:
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对象,其中包含两个重要的字段:
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 }
web3.eth.accounts
返回的 第一个帐户,做为部署这个合约的帐户。在提交交易以前,你必须拥有并解锁这个帐户。不过为了方便 起见,ganache
默认会自动解锁这10个帐户。咱们已经成功部署了投票合约,而且得到了一个合约实例(变量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
页面,以便用户可使用浏览器 而不是复杂的命令行来与投票合约交互:
页面的主要功能以下:
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开发项目可参考项目
Truffle
是一个DApp开发框架,它简化了去中心化应用的构建和管理。你能够在 这里了解框架的 更多内容和完整特性。
若是你须要在本身的机器上安装,可使用 npm 全局安装:npm install -g truffle
end