浅谈区块链

5,532 阅读10分钟
原文链接: zhuanlan.zhihu.com

最近一段时间,区块链技术频繁出现在各种新闻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