最近一段时间,区块链技术频繁出现在各种新闻APP的头条里,在这种情况下,我开始了对区块链技术的探索。
概念介绍
区块链技术中涌现很多新的概念,而这对于初入区块链领域的人来说,相当不友好,我在很长一段时间里,都没有搞清楚区块链是什么,虚拟钱包是什么,为什么交易有gas费用等等,所以本文首先会给出一些概念的解释,方便后面的探讨。
区块链
区块链其实就是一种链表结构,链表中的元素就是一个区块,每一个区块结构如下:
- timestamp: 区块产生时间戳
- nonce: 与区块头的hash值共同证明计算量(工作量)
- data: 区块链上存储的数据
- prevHash: 上一个区块的hash
- hash: 本区块链的hash,由上述几个属性进行哈希计算而得
区块链本质是一种分布式的交易账本,所有用户都在本地存有完整账本信息。如果有用户想改变某一个区块信息,由于区块 hash 的计算过程使用了 prevHash 作为参数,那么该区块后的所有区块,都会变得不合法,需要重新计算 hash ,想让系统承认这个更改,必须同步更改 51% 的用户的账本信息,所以篡改区块链上的账本信息十分困难,这就保证了它的安全性。
矿工与挖矿
挖矿本质上是一组节点(矿机)使用他们的计算资源去创建一个包含有效交易的区块的过程,参与这个过程的节点(矿机)被称为矿工。一个矿工想要提交一个区块到区块链上,就必须更快的计算出一个nonce,nonce 和 区块头信息能共同证明,一个区块是有效的。
工作量证明
上面提到,挖矿的过程中,矿工必须更快的计算出一个 nonce,这个 nonce 如何计算呢?nonce 是一个整数值,一般先把区块头信息后面加上nonce得到的字符串,进行 SHA256 哈希运算,得到的结果如果开头0的个数小于设定的难度值,则验证不通过,把 nonce 值加1重复上述操作,直到计算出来的 nonce 满足得到的哈希值开头0的个数不小于设定的难度值。而nonce的值,就是挖矿过程中工作量的证明。而系统为了鼓励更多矿工参与进来,会给参与挖矿的每个矿工一定代币的奖励。
钱包
钱包本质是一个包含私钥的文件。通常会包含一个软件客户端,钱包的地址,是由私钥计算出来的,也就是公钥。每一次交易,发送方必须要提供私钥,才能把该公钥地址下所拥有的代币转帐到其他公钥地址,所以私钥决定了比特币的所有权。这里要注意一点,一个钱包地址拥有多少代币,不是存储在私钥里,而是存在区块链上,区块链上有着所有历史交易账目,可以根据账目计算出每个地址所拥有的代币。
比特币 VS 以太坊
区块链目前最火的两个应用就是比特币系统和以太坊系统,这两个系统都是公共区块链平台,都有自己自己的虚拟货币(比特币 和 以太坊)。但是他们是有很多区别的,其中一个重要区别就是,以太坊通过智能合约使平台具有图灵完备性,相对于比特币,未来更具有扩展性。当然这也和他们本身的目标有关,比特币想成为纯粹的虚拟货币,以太坊想成为一个纯粹的图灵完备的开发平台。
智能合约
终于把一些基本概念普及完了,接下来该介绍重点,智能合约的开发了。
首先介绍一下需要使用的工具
geth
Geth是由以太坊基金会提供的官方客户端软件,用Go编程语言编写的。
testrpc
testrpc 和 geth 不同,geth是以太坊公链环境,testrpc 是本地模拟的一个以太坊环境,便于测试开发。
MetaMask
MetaMask 是一个基于chrome插件开发的一个钱包。
Remix
以太坊官方推荐的智能合约开发IDE,可以在浏览器中快速部署测试智能合约。
Solidity
Solidity 官方推出的用于编写智能合约的最流行的编程语言。
Truffle
用于编译、部署智能合约的工具
Web3.js
Javascript 库,用于和节点交互,可以用于构建基于web的Dapp。
智能合约开发
安装 testrpc
npm install -g ethereumjs-testrpc # 安装 testrpc
testprc # 本地模拟以太坊环境
EthereumJS TestRPC v6.0.3 (ganache-core: 2.0.2)
Available Accounts
==================
(0) 0xca56a0e708a9b8268ea6d7b3e93728a6a324d628
(1) 0xb9b3eb735a71e0b1ae55505fa25423298c9e0ba1
(2) 0x8c0b3356fb423f2c67216627f61e2bee329ccf66
(3) 0xd67270a1a8b2bfb2045cd717307ca5475e093797
(4) 0xd2d442b9d7bb46edd53953f3c0bbae00deca9b56
(5) 0x21ef73d1bd2db40c35c169f33d04035c4fa04723
(6) 0x9a6e56ae9026abe47dd3e5b7e337d11c95e255a3
(7) 0x16e9e712af18ca96a3323aa794ce437dda348a73
(8) 0x753630b2546fdea270d380e61cbdf98b8d612f31
(9) 0x6931462530e8359cb8ff70802290065d033301e0
Private Keys
==================
(0) 0e827c0d81130b789945254cffe2d42d043ecfedc4b5b9149b9f6f8321a40a80
(1) 081824aba4f6adb70b32141600f3edf847a7bd736328104f088a1f120d5bc39b
(2) 7834de28a0d82155b56df3bf5c1e1e12d86e7de69f92850fc2b60373a25cc995
(3) 56242f1868b8da44804f07180d7d2f01ef6132e98cedd5ef9c53b9609c40cf71
(4) 955baf7ab9daa5cdbc3d1a535494f2730216f967b5d30d8db4e756376b6d6ce1
(5) b0b741a135fa5fb6011bd29c65b1dc6f72af9862630e671e85c76d43fb3db134
(6) 165db38471c04b6b87196c050f83bae2d9e6e3c14f09c61d2b7de6208ad62623
(7) c6d5523eb0f233f09d7359bb7b73f347ff1ed5efea63ca1aacd0ec52da6a8b76
(8) 0057d1448842232df2bdc5e0d8c9737dadb50aa392ad1484ec4ce592089057ba
(9) 5c574a3bd2d2c1ee4cc70bfe116b1d99dc8da107d90c116444139867f74f86c2
HD Wallet
==================
Mnemonic: tree embark shuffle foil screen transfer struggle exotic any during stage response
Base HD Path: m/44'/60'/0'/0/{account_index}
Listening on localhost:8545
第一个账户给了100个以太坊,可以用来开发测试时使用,在 MetaMask 上选择本地测试网络,即可查看账户余额。
智能合约 hello world 版本
pragma solidity ^0.4.14; // 定义 solidity 版本
contract greeter {
string greeting;
function greeter(string _greeting) public { // 构造函数,在合约创建时执行
greeting = _greeting;
}
function greet() public constant returns (string) { // 功能函数
return greeting;
}
}
实现一个智能合约版投票系统
定义角色数据类型
pragma solidity ^0.4.14; // 定义 solidity 版本
contract VoteDemo {
struct Voter // 定义投票人
{
bool voted; // 是否已投票
address addr; // 投票地址
bytes32 voteProposalName; // 选择提案的索引
}
struct Proposal // 定义提案
{
bytes32 name; // 提案名称
uint voteCount; // 累积得票数
}
address public voteCreator; // 投票发起人
Voter[] public voters; // 所有已授权的投票人
Proposal[] public proposals; // 所有的提案
}
构造函数
// 创建一个提案,给出所有提案选项
function VoteDemo(bytes32[] proposalNames) public {
for(uint i = 0; i < proposalNames.length; i++) { // 初始化所有提案
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
功能函数
// 验证提案是否合法
function checkProposalValid(bytes32 proposalName) public view returns(bool isValid) {
isValid = false;
for (uint i = 0; i < proposals.length; i++) {
if (proposals[i].name == proposalName) {
isValid = true;
}
}
}
// 获取某个提案的投票数
function getVoteNumByProposal(bytes32 proposalName) public view returns(uint voteCount) {
require(checkProposalValid(proposalName));
for (uint i = 0; i < proposals.length; i++) {
if (proposals[i].name == proposalName) {
voteCount = proposals[i].voteCount;
}
}
}
// 获取所有提案
function getProposalList() public view returns(Proposal[]) {
return proposals;
}
// 获取投票情况
function getVotedList() public view returns(Voter[]) {
Voter[] memory res;
for (uint i = 0; i < voters.length; i++) {
if (voters[i].voted == true) {
res[i] = Voter({
voted: voters[i].voted,
voteProposalName: voters[i].voteProposalName,
addr: voters[i].addr
});
}
}
return res;
}
// 验证投票人是否有权限参加投票
function isValidVoter(address addr) public view returns(bool isValid) {
isValid = false;
for (uint i = 0; i < voters.length; i++) {
if (voters[i].addr == addr) {
isValid = true;
}
}
}
// 添加投票人
function addVoter(address addr) public {
require(msg.sender == voteCreator);
voters.push(Voter({
voted: false,
addr: addr,
voteProposalName: ''
}));
}
// 投票
function vote(bytes32 proposalName) public {
require(isValidVoter(msg.sender));
for (uint i = 0; i < voters.length; i++) {
if (msg.sender == voters[i].addr) {
voters[i].voteProposalName = proposalName;
voters[i].voted = true;
}
}
}
到这里,一个简单的智能合约版本的投票系统就写完了,接下来,该看看如何部署。
获取智能合约字节码(byte code)和 二进制接口(ABI)
将上面的智能合约代码拷贝到 remix 中,编译之后,获取 ABI 和 byte code
建立私链
由于在公链上部署合约和调用合约都需要花费一定的以太坊代币,所以在开发和测试阶段,可以自行搭建以太坊私链。
首先下载并编译geth
curl -o go-ethereum.tar.gz https://github.com/ethereum/go-ethereum/archive/v1.7.3.tar.gz
tar zxvf go-ethereum.tar.gz
cd go-ethereum
make geth
mv build/bin/geth /usr/local/bin
建立私链,运行节点
geth --identity [nodeId] --dev --datadir [datadir] --rpc --rpcaddr [yourIp] --rpcport [rpc port] --port [port]
--identity
自定义节点ID--dev
开发模式--datadir
私链数据储存目录--rpc
开启rpc 服务--rpcaddr
rpc 服务地址--rpcport
rpc 服务端口--port
指定和其他节点连接所有的端口号
这时候如果打开指定的数据储存目录,会发现有一个geth.ipc 文件,
连接私链
使用 geth attach 连接私链节点,并打开 geth console。
geth attach ipc:[your ipc file]
geth console 是一个交互式的 JavaScript 环境。在这个环境中内置以下用来操作以太坊的对象:
eth:
包含操作区块链相关的方法;net:
包含查看p2p网络状态的方法;admin:
包含与管理节点相关的方法;miner:
包含启动&停止挖矿的方法;personal:
主要包含管理账户的方法;txpool:
包含查看交易内存池的方法;web3:
包含了以上对象,还包含单位换算的方法。
web3 相关的API,可以查阅 web3.js - Ethereum JavaScript API 。
部署合约
var abi = JSON.parse(abiString) // 将前面得到的 ABI JSON 字符串转换为对象
var bytecode = 'bytecodeString' // 前面所得到的 byte code
var contract = web3.eth.contract(abi) // 根据ABI得到智能合约
var account = web3.personal.listAccounts[0]
var instance = contract.new({
data: bytecode,
gas: 1000000,
from: account
})
这样,合约就部署在私链上了,现在可以调用合约中的方法
instance.xxx() // 调用合约中的方法
总结
目前区块链技术可谓是炙手可热,作为前端的我们,可以尝试了解一些区块链基本知识,了解如何利用 web3.js 开发与公链节点交互的前端应用。现在已经有很多优秀的脚手架,可以让我们快速开始开发基于以太坊智能合约的前端应用,这里可以推荐一下 leopoldjoy/react-ethereum-dapp-example 。