君子和而不同。
在区块链领域,以太坊项目同样是十分出名的开源项目。作为公有区块链平台,以太坊将比特币针对数字货币交易的功能进一步进行拓展,面向更为复杂和灵活的应用场景,支持了智能合约(Smart Contract)这一重要特性。
从此,区块链技术的应用场景,从单一基于 UTXO 的数字货币交易,延伸到图灵完备的通用计算领域。用户不再受限于仅能使用比特币脚本所支持的简单逻辑,而是可以自行设计任意复杂的合约逻辑。这就为构建各种多样化的上层应用开启了大门,可谓意义重大。
本章将参照比特币项目来介绍以太坊项目的核心概念和改进设计,以及如何安装客户端和使用智能合约等内容。
以太坊项目简介
图 1.10.1.1 - 以太坊项目
以太坊(Ethereum)项目的最初目标,是打造一个运行智能合约的平台(Platform for Smart Contract)。该平台支持图灵完备的应用,按照智能合约的约定逻辑自动执行,理想情况下将不存在故障停机、审查、欺诈,以及第三方干预等问题。
以太坊平台目前支持 Golang、C++、Python 等多种语言实现的客户端。由于核心实现上基于比特币网络的核心思想进行了拓展,因此在很多设计特性上都与比特币网络十分类似。
基于以太坊项目,以太坊团队目前运营了一条公开的区块链平台——以太坊网络。智能合约开发者使用官方提供的工具和以太坊专用应用开发语言 Solidity,可以很容易开发出运行在以太坊网络上的“去中心化”应用(Decentralized Application,DApp)。这些应用将运行在以太坊的虚拟机(Ethereum Virtual Machine,EVM)里。用户通过以太币(Ether)来购买燃料(Gas),维持所部署应用的运行。
以太坊项目的官网网站为 ethereum.org,代码托管在 github.com/ethereum。
以太坊项目简史
相对比特币网络自 2009 年上线的历史,以太坊项目要年轻的多。
2013 年底,比特币开发团队中有一些开发者开始探讨将比特币网络中的核心技术,主要是区块链技术,拓展到更多应用场景的可能性。以太坊的早期发明者 Vitalik Buterin 提出应该能运行任意形式(图灵完备)的应用程序,而不仅仅是比特币中受限制的简单脚本。该设计思想并未得到比特币社区的支持,后来作为以太坊白皮书发布。
2014 年 2 月,更多开发者(包括 Gavin Wood、Jeffrey Wilcke 等)加入以太坊项目,并计划在社区开始以众筹形式募集资金,以开发一个运行智能合约的信任平台。
2014 年 7 月,以太币预售,经过 42 天,总共筹集到价值超过 1800 万美金的比特币。随后在瑞士成立以太坊基金会,负责对募集到的资金进行管理和运营;并组建研发团队以开源社区形式进行平台开发。
2015 年 7 月底,以太坊第一阶段 Frontier 正式发布,标志着以太坊区块链网络的正式上线。这一阶段采用类似比特币网络的 PoW 共识机制,参与节点以矿工挖矿形式维护网络;支持上传智能合约。Frontier 版本实现了计划的基本功能,在运行中测试出了一些安全上的漏洞。这一阶段使用者以开发者居多。
2016 年 3 月,第二阶段 Homestead 开始运行(区块数 1150000),主要改善了安全性,同时开始提供图形界面的客户端,提升了易用性,更多用户加入进来。
2016 年 6 月,DAO 基于以太坊平台进行众筹,受到漏洞攻击,造成价值超过 5000 万美金的以太币被冻结。社区最后通过硬分叉(Hard Fork)进行解决。
2017 年 3 月,以太坊成立以太坊企业级联盟(Enterprise Ethereum Alliance,EEA),联盟成员主要来自摩根大通,微软,芝加哥大学和部分创业企业等。
2017 年 11 月,再次暴露多签名钱包漏洞,造成价值 2.8 亿美元的以太币被冻结。
目前,以太坊网络支持了接近比特币网络的交易量,成为广受关注的公有链项目。
后续按照计划将发布第三阶段 Metropolis 和第四阶段 Serenity,主要特性包括支持 PoS 股权证明的共识机制,以降低原先 PoW 机制造成的能耗浪费;以及图形界面的钱包,以提升易用性。
包括 DAO 在内,以太坊网络已经经历了数次大的硬分叉,注意每次硬分叉后的版本对之前版本并不兼容。
主要特点
以太坊区块链底层也是一个类似比特币网络的 P2P 网络平台,智能合约运行在网络中的以太坊虚拟机里。网络自身是公开可接入的,任何人都可以接入并参与网络中数据的维护,提供运行以太坊虚拟机的资源。
跟比特币项目相比,以太坊区块链的技术特点主要包括:
- 支持图灵完备的智能合约,设计了编程语言 Solidity 和虚拟机 EVM;
- 选用了内存需求较高的哈希函数,避免出现强算力矿机、矿池攻击;
- 叔块(Uncle Block)激励机制,降低矿池的优势,并减少区块产生间隔(10 分钟降低到 15 秒左右);
- 采用账户系统和世界状态,而不是 UTXO,容易支持更复杂的逻辑;
- 通过 Gas 限制代码执行指令数,避免循环执行攻击;
- 支持 PoW 共识算法,并计划支持效率更高的 PoS 算法。
此外,开发团队还计划通过分片(Sharding)方式来解决网络可扩展性问题。
这些技术特点,解决了比特币网络在运行中被人诟病的一些问题,让以太坊网络具备了更大的应用潜力。
核心概念
基于比特币网络的核心思想,以太坊项目提出了许多创新的技术概念,包括智能合约、基于账户的交易、以太币和燃料等。
智能合约
智能合约(Smart Contract)是以太坊中最为重要的一个概念,即以计算机程序的方式来缔结和运行各种合约。最早在上世纪 90 年代,Nick Szabo 等人就提出过类似的概念,但一直依赖因为缺乏可靠执行智能合约的环境,而被作为一种理论设计。区块链技术的出现,恰好补充了这一缺陷。
以太坊支持通过图灵完备的高级语言(包括 Solidity、Serpent、Viper)等来开发智能合约。智能合约作为运行在以太坊虚拟机(Ethereum Virual Machine,EVM)中的应用,可以接受来自外部的交易请求和事件,通过触发运行提前编写好的代码逻辑,进一步生成新的交易和事件,可以进一步调用其它智能合约。
智能合约的执行结果可能对以太坊网络上的账本状态进行更新。这些修改由于经过了以太坊网络中的共识,一旦确认后无法被伪造和篡改。
账户
在之前章节中,笔者介绍过比特币在设计中并没有账户(Account)的概念,而是采用了 UTXO 模型记录整个系统的状态。任何人都可以通过交易历史来推算出用户的余额信息。而以太坊则采用了不同的做法,直接用账户来记录系统状态。每个账户存储余额信息、智能合约代码和内部数据存储等。以太坊支持在不同的账户之间转移数据,以实现更为复杂的逻辑。
具体来看,以太坊账户分为两种类型:合约账户(Contracts Accounts)和外部账户(Externally Owned Accounts,或 EOA)。
- 合约账户:存储执行的智能合约代码,只能被外部账户来调用激活;
- 外部账户:以太币拥有者账户,对应到某公钥。账户包括 nonce、balance、storageRoot、codeHash 等字段,由个人来控制。
当合约账户被调用时,存储其中的智能合约会在矿工处的虚拟机中自动执行,并消耗一定的燃料。燃料通过外部账户中的以太币进行购买。
交易
交易(Transaction),在以太坊中是指从一个账户到另一个账户的消息数据。消息数据可以是以太币或者合约执行参数。
以太坊采用交易作为执行操作的最小单位。每个交易包括如下字段:
- to:目标账户地址。
- value:可以指定转移的以太币数量。
- nonce:交易相关的字串,用于防止交易被重放。
- gasPrice:执行交易需要消耗的 Gas 价格。
- gasLimit:交易消耗的最大 Gas 值。
- data: 交易附带字节码信息,可用于创建/调用智能合约。
- signature:签名信息。
类似比特币网络,在发送交易时,用户需要缴纳一定的交易费用,通过以太币方式进行支付和消耗。目前,以太坊网络可以支持超过比特币网络的交易速率(可以达到每秒几十笔)。
以太币
以太币(Ether)是以太坊网络中的货币。
以太币主要用于购买燃料,支付给矿工,以维护以太坊网络运行智能合约的费用。以太币最小单位是 wei,一个以太币等于 10^18 个 wei。
以太币同样可以通过挖矿来生成,成功生成新区块的以太坊矿工可以获得 3 个以太币的奖励,以及包含在区块内交易的燃料费用。用户也可以通过交易市场来直接购买以太币。
目前每年大约可以通过挖矿生成超过一千万个以太币,单个以太币的市场价格目前超过 300 美金。
燃料
燃料(Gas),控制某次交易执行指令的上限。每执行一条合约指令会消耗固定的燃料。当某个交易还未执行结束,而燃料消耗完时,合约执行终止并回滚状态。
Gas 可以跟以太币进行兑换。需要注意的是,以太币的价格是波动的,但运行某段智能合约的燃料费用可以是固定的,通过设定 Gas 价格等进行调节。
主要设计
以太坊项目的基本设计与比特币网络类似。为了支持更复杂的智能合约,以太坊在不少地方进行了改进,包括交易模型、共识、对攻击的防护和可扩展性等。
智能合约相关设计
运行环境
以太坊采用以太坊虚拟机作为智能合约的运行环境。以太坊虚拟机是一个隔离的轻量级虚拟机环境,运行在其中的智能合约代码无法访问本地网络、文件系统或其它进程。
对同一个智能合约来说,往往需要在多个以太坊虚拟机中同时运行多份,以确保整个区块链数据的一致性和高度的容错性。另一方面,这也限制了整个网络的容量。
开发语言
以太坊为编写智能合约设计了图灵完备的高级编程语言,降低了智能合约开发的难度。
目前 Solidity 是最常用的以太坊合约编写语言之一。
智能合约编写完毕后,用编译器编译为以太坊虚拟机专用的二进制格式(EVM bytecode),由客户端上传到区块链当中,之后在矿工的以太坊虚拟机中执行。
交易模型
出于智能合约的便利考虑,以太坊采用了账户的模型,状态可以实时的保存到账户里,而无需像比特币的 UXTO 模型那样去回溯整个历史。
UXTO 模型和账户模型的对比如下。
特性 | UXTO 模型 | 账户模型 |
---|---|---|
状态查询和变更 | 需要回溯历史 | 直接访问 |
存储空间 | 较大 | 较小 |
易用性 | 较难处理 | 易于理解和编程 |
安全性 | 较好 | 需要处理好重放攻击等情况 |
可追溯性 | 支持历史 | 不支持追溯历史 |
共识
以太坊目前采用了基于成熟的 PoW 共识的变种算法 Ethash 协议作为共识机制。
为了防止 ASIC 矿机矿池的算力攻击,跟原始 PoW 的计算密集型 Hash 运算不同,Ethash 在执行时候需要消耗大量内存,反而跟计算效率关系不大。这意味着很难制造出专门针对 Ethash 的芯片,反而是通用机器可能更加有效。
虽然,Ethash 相对原始的 PoW 进行了改进,但仍然需要进行大量无效的运算,这也为人们所诟病。
社区已经有计划在未来采用更高效的 Proof-of-Stake(PoS)作为共识机制。相对 PoW 机制来讲,PoS 机制无需消耗大量无用的 Hash 计算,但其共识过程的复杂度要更高一些,还有待进一步的检验。
降低攻击
以太坊网络中的交易更加多样化,也就更容易受到攻击。
以太坊网络在降低攻击方面的核心设计思想,仍然是通过经济激励机制防止少数人作恶:
- 所有交易都要提供交易费用,避免 DDoS 攻击;
- 程序运行指令数通过 Gas 来限制,所消耗的费用超过设定上限时就会被取消,避免出现恶意合约。
这就确保了攻击者试图消耗网络中虚拟机的计算资源时,需要付出经济代价(支付大量的以太币);同时难以通过构造恶意的循环或不稳定合约代码来对网络造成破坏。
提高扩展性
可扩展性是以太坊网络承接更多业务量的最大制约。
以太坊项目未来希望通过分片(sharding)机制来提高整个网络的扩展性。
分片是一组维护和执行同一批智能合约的节点组成的子网络,是整个网络的子集。
支持分片功能之前,以太坊整个网络中的每个节点都需要处理所有的智能合约,这就造成了网络的最大处理能力会受限于单个节点的处理能力。
分片后,同一片内的合约处理是同步的,彼此达成共识,不同分片之间则可以是异步的,可以提高网络整体的可扩展性。
相关工具
客户端和开发库
以太坊客户端可用于接入以太坊网络,进行账户管理、交易、挖矿、智能合约等各方面操作。
以太坊社区现在提供了多种语言实现的客户端和开发库,支持标准的 JSON-RPC 协议。用户可根据自己熟悉的开发语言进行选择。
- go-ethereum:Go 语言实现;
- Parity:Rust 语言实现;
- cpp-ethereum:C++ 语言实现;
- ethereumjs-lib:javascript 语言实现;
- Ethereum(J):Java 语言实现;
- ethereumH:Haskell 语言实现;
- pyethapp:Python 语言实现;
- ruby-ethereum:Ruby 语言实现。
Geth
上述实现中,go-ethereum 的独立客户端 Geth 是最常用的以太坊客户端之一。
用户可通过安装 Geth 来接入以太坊网络并成为一个完整节点。Geth 也可作为一个 HTTP-RPC 服务器,对外暴露 JSON-RPC 接口,供用户与以太坊网络交互。
Geth 的使用需要基本的命令行基础,其功能相对完整,源码托管于 github.com/ethereum/go-ethereum。
以太坊钱包
对于只需进行账户管理、以太坊转账、DApp 使用等基本操作的用户,则可选择直观易用的钱包客户端。
Mist 是官方提供的一套包含图形界面的钱包客户端,除了可用于进行交易,也支持直接编写和部署智能合约。
图 1.10.4.1 - Mist 浏览器
所编写的代码编译发布后,可以部署到区块链上。使用者可通过发送调用相应合约方法的交易,来执行智能合约。
IDE
对于开发者,以太坊社区涌现出许多服务于编写智能合约和 DApp 的 IDE,例如:
- Truffle:一个功能丰富的以太坊应用开发环境。
- Embark:一个 DApp 开发框架,支持集成以太坊、IPFS 等。
- Remix:一个用于编写 Solidity 的 IDE,内置调试器和测试环境。
网站资源
已有一些网站提供对以太坊网络的数据、运行在以太坊上的 DApp 等信息进行查看,例如:
- ethstats.net:实时查看网络的信息,如区块、价格、交易数等。
- ethernodes.org:显示整个网络的历史统计信息,如客户端的分布情况等。
- dapps.ethercasts.com:查看运行在以太坊上的 DApp 的信息,包括简介、所处阶段和状态等。
图 1.10.4.2 - 以太坊网络上的 Dapp 信息
安装客户端
本节将介绍如何安装 Geth,即 Go 语言实现的以太坊客户端。这里以 Ubuntu 16.04 操作系统为例,介绍从 PPA 仓库和从源码编译这两种方式来进行安装。
从 PPA 直接安装
首先安装必要的工具包。
$ apt-get install software-properties-common
之后用以下命令添加以太坊的源。
$ add-apt-repository -y ppa:ethereum/ethereum
$ apt-get update
最后安装 go-ethereum。
$ apt-get install ethereum
安装成功后,则可以开始使用命令行客户端 Geth。可用 geth --help
查看各命令和选项,例如,用以下命令可查看 Geth 版本为 1.6.1-stable。
$ geth version
Geth
Version: 1.6.1-stable
Git Commit: 021c3c281629baf2eae967dc2f0a7532ddfdc1fb
Architecture: amd64
Protocol Versions: [63 62]
Network Id: 1
Go Version: go1.8.1
Operating System: linux
GOPATH=
GOROOT=/usr/lib/go-1.8
从源码编译
也可以选择从源码进行编译安装。
安装 Go 语言环境
Go 语言环境可以自行访问 golang.org 网站下载二进制压缩包安装。注意不推荐通过包管理器安装版本,往往比较旧。
如下载 Go 1.8 版本,可以采用如下命令。
$ curl -O https://storage.googleapis.com/golang/go1.8.linux-amd64.tar.gz
下载完成后,解压目录,并移动到合适的位置(推荐为 /usr/local 下)。
$ tar -xvf go1.8.linux-amd64.tar.gz
$ sudo mv go /usr/local
安装完成后记得配置 GOPATH 环境变量。
$ export GOPATH=YOUR_LOCAL_GO_PATH/Go
$ export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
此时,可以通过 go version
命令验证安装 是否成功。
$ go version
go version go1.8 linux/amd64
下载和编译 Geth
用以下命令安装 C 的编译器。
$ apt-get install -y build-essential
下载选定的 go-ethereum 源码版本,如最新的社区版本:
$ git clone https://github.com/ethereum/go-ethereum
编译安装 Geth。
$ cd go-ethereum
$ make geth
安装成功后,可用 build/bin/geth --help
查看各命令和选项。例如,用以下命令可查看 Geth 版本为 1.6.3-unstable。
$ build/bin/geth version
Geth
Version: 1.6.3-unstable
Git Commit: 067dc2cbf5121541aea8c6089ac42ce07582ead1
Architecture: amd64
Protocol Versions: [63 62]
Network Id: 1
Go Version: go1.8
Operating System: linux
GOPATH=/usr/local/gopath/
GOROOT=/usr/local/go
使用智能合约
以太坊社区有不少提供智能合约编写、编译、发布、调用等功能的工具,用户和开发者可以根据需求或开发环境自行选择。
本节将向开发者介绍使用 Geth 客户端搭建测试用的本地区块链,以及如何在链上部署和调用智能合约。
搭建测试用区块链
由于在以太坊公链上测试智能合约需要消耗以太币,所以对于开发者开发测试场景,可以选择本地自行搭建一条测试链。开发好的智能合约可以很容易的切换接口部署到公有链上。注意测试链不同于以太坊公链,需要给出一些非默认的手动配置。
配置初始状态
首先配置私有区块链网络的初始状态。新建文件 genesis.json
,内容如下。
{
"config": {
"chainId": 22,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"alloc" : {},
"coinbase" : "0x0000000000000000000000000000000000000000",
"difficulty" : "0x400",
"extraData" : "",
"gasLimit" : "0x2fefd8",
"nonce" : "0x0000000000000038",
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp" : "0x00"
}
其中,chainId
指定了独立的区块链网络 ID,不同 ID 网络的节点无法互相连接。配置文件还对当前挖矿难度 difficulty
、区块 Gas 消耗限制 gasLimit
等参数进行了设置。
启动区块链
用以下命令初始化区块链,生成创世区块和初始状态。
$ geth --datadir /path/to/datadir init /path/to/genesis.json
其中,--datadir
指定区块链数据的存储位置,可自行选择一个目录地址。
接下来用以下命令启动节点,并进入 Geth 命令行界面。
$ geth --identity "TestNode" --rpc --rpcport "8545" --datadir /path/to/datadir --port "30303" --nodiscover console
各选项的含义如下。
--identity
:指定节点 ID;--rpc
: 表示开启 HTTP-RPC 服务;--rpcport
: 指定 HTTP-RPC 服务监听端口号(默认为 8545);--datadir
: 指定区块链数据的存储位置;--port
: 指定和其他节点连接所用的端口号(默认为 30303);--nodiscover
: 关闭节点发现机制,防止加入有同样初始配置的陌生节点;
创建账号
用上述 geth console
命令进入的命令行界面采用 JavaScript 语法。可以用以下命令新建一个账号。
> personal.newAccount()
Passphrase:
Repeat passphrase:
"0x1b6eaa5c016af9a3d7549c8679966311183f129e"
输入两遍密码后,会显示生成的账号,如"0x1b6eaa5c016af9a3d7549c8679966311183f129e"
。可以用以下命令查看该账号余额。
> myAddress = "0x1b6eaa5c016af9a3d7549c8679966311183f129e"
> eth.getBalance(myAddress)
0
看到该账号当前余额为 0。可用 miner.start()
命令进行挖矿,由于初始难度设置的较小,所以很容易就可挖出一些余额。miner.stop()
命令可以停止挖矿。
创建和编译智能合约
以 Solidity 编写的智能合约为例。为了将合约代码编译为 EVM 二进制,需要安装 Solidity 编译器 solc。
$ apt-get install solc
新建一个 Solidity 智能合约文件,命名为 testContract.sol
,内容如下。该合约包含一个方法 multiply,作用是将输入的整数乘以 7 后输出。
pragma solidity ^0.4.0;
contract testContract {
function multiply(uint a) returns(uint d) {
d = a * 7;
}
}
用 solc 获得合约编译后的 EVM 二进制码。
$ solc --bin testContract.sol
======= testContract.sol:testContract =======
Binary:
6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063c6888fa114603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60006007820290505b9190505600a165627a7a72305820748467daab52f2f1a63180df2c4926f3431a2aa82dcdfbcbde5e7d036742a94b0029
再用 solc 获得合约的 JSON ABI(Application Binary Interface),其中指定了合约接口,包括可调用的合约方法、变量、事件等。
$ solc --abi testContract.sol
======= testContract.sol:testContract =======
Contract JSON ABI
[{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"payable":false,"type":"function"}]
下面回到 Geth 的 JavaScript 环境命令行界面,用变量记录上述两个值。注意在 code 前加上 0x
前缀。
> code = "0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063c6888fa114603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60006007820290505b9190505600a165627a7a72305820748467daab52f2f1a63180df2c4926f3431a2aa82dcdfbcbde5e7d036742a94b0029"
> abi = [{"constant":false,"inputs":[{"name":"a","type":"uint256"}],"name":"multiply","outputs":[{"name":"d","type":"uint256"}],"payable":false,"type":"function"}]
部署智能合约
在 Geth 的 JavaScript 环境命令行界面,首先用以下命令解锁自己的账户,否则无法发送交易。
> personal.unlockAccount(myAddress)
Unlock account 0x1b6eaa5c016af9a3d7549c8679966311183f129e
Passphrase:
true
接下来发送部署合约的交易。
> myContract = eth.contract(abi)
> contract = myContract.new({from:myAddress,data:code,gas:1000000})
如果此时没有在挖矿,用 txpool.status
命令可看到本地交易池中有一个待确认的交易。可用以下命令查看当前待确认的交易。
> eth.getBlock("pending",true).transactions
[{
blockHash: "0xbf0619ca48d9e3cc27cd0ab0b433a49a2b1bed90ab57c0357071b033aca1f2cf",
blockNumber: 17,
from: "0x1b6eaa5c016af9a3d7549c8679966311183f129e",
gas: 90000,
gasPrice: 20000000000,
hash: "0xa019c2e5367b3ad2bbfa427b046ab65c81ce2590672a512cc973b84610eee53e",
input: "0x6060604052341561000c57fe5b5b60a58061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063c6888fa114603a575bfe5b3415604157fe5b60556004808035906020019091905050606b565b6040518082815260200191505060405180910390f35b60006007820290505b9190505600a165627a7a72305820748467daab52f2f1a63180df2c4926f3431a2aa82dcdfbcbde5e7d036742a94b0029",
nonce: 1,
r: "0xbcb2ba94f45dfb900a0533be3c2c603c2b358774e5fe89f3344031b202995a41",
s: "0x5f55fb1f76aa11953e12746bc2d19fbea6aeb1b9f9f1c53a2eefab7058515d99",
to: null,
transactionIndex: 0,
v: "0x4f",
value: 0
}]
可以用 miner.start()
命令挖矿,一段时间后,交易会被确认,即随新区块进入区块链。
调用智能合约
用以下命令可以发送交易,其中 sendTransaction 方法的前几个参数与合约中 multiply 方法的输入参数对应。这种方式,交易会通过挖矿记录到区块链中,如果涉及状态改变也会获得全网共识。
> contract.multiply.sendTransaction(10, {from:myAddress})
如果只是想本地运行该方法查看返回结果,可采用如下方式获取结果。
> contract.multiply.call(10)
70
智能合约案例:投票
本节将介绍一个用 Solidity 语言编写的智能合约案例。代码来源于 Solidity 官方文档 中的示例。
该智能合约实现了一个自动化的、透明的投票应用。投票发起人可以发起投票,将投票权赋予投票人;投票人可以自己投票,或将自己的票委托给其他投票人;任何人都可以公开查询投票的结果。
智能合约代码
实现上述功能的合约代码如下所示,并不复杂,语法跟 JavaScript 十分类似。
pragma solidity ^0.4.11;
contract Ballot {
struct Voter {
uint weight;
bool voted;
address delegate;
uint vote;
}
struct Proposal {
bytes32 name;
uint voteCount;
}
address public chairperson;
mapping(address => Voter) public voters;
Proposal[] public proposals;
// Create a new ballot to choose one of `proposalNames`
function Ballot(bytes32[] proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1;
for (uint i = 0; i < proposalNames.length; i++) {
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
// Give `voter` the right to vote on this ballot.
// May only be called by `chairperson`.
function giveRightToVote(address voter) {
require((msg.sender == chairperson) && !voters[voter].voted);
voters[voter].weight = 1;
}
// Delegate your vote to the voter `to`.
function delegate(address to) {
Voter sender = voters[msg.sender];
require(!sender.voted);
require(to != msg.sender);
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
// We found a loop in the delegation, not allowed.
require(to != msg.sender);
}
sender.voted = true;
sender.delegate = to;
Voter delegate = voters[to];
if (delegate.voted) {
proposals[delegate.vote].voteCount += sender.weight;
} else {
delegate.weight += sender.weight;
}
}
// Give your vote (including votes delegated to you)
// to proposal `proposals[proposal].name`.
function vote(uint proposal) {
Voter sender = voters[msg.sender];
require(!sender.voted);
sender.voted = true;
sender.vote = proposal;
proposals[proposal].voteCount += sender.weight;
}
// @dev Computes the winning proposal taking all
// previous votes into account.
function winningProposal() constant
returns (uint winningProposal)
{
uint winningVoteCount = 0;
for (uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal = p;
}
}
}
// Calls winningProposal() function to get the index
// of the winner contained in the proposals array and then
// returns the name of the winner
function winnerName() constant
returns (bytes32 winnerName)
{
winnerName = proposals[winningProposal()].name;
}
}
代码解析
指定版本
在第一行,pragma
关键字指定了和该合约兼容的编译器版本。
pragma solidity ^0.4.11;
该合约指定,不兼容比 0.4.11
更旧的编译器版本,且 ^
符号表示也不兼容从 0.5.0
起的新编译器版本。即兼容版本范围是 0.4.11 <= version < 0.5.0
。该语法与 npm 的版本描述语法一致。
结构体类型
Solidity 中的合约(contract)类似面向对象编程语言中的类。每个合约可以包含状态变量、函数、事件、结构体类型和枚举类型等。一个合约也可以继承另一个合约。
在本例命名为 Ballot
的合约中,声明了 2 个结构体类型:Voter
和 Proposal
。
struct Voter
:投票人,其属性包括uint weight
(该投票人的权重)、bool voted
(是否已投票)、address delegate
(如果该投票人将投票委托给他人,则记录受委托人的账户地址)和uint vote
(投票做出的选择,即相应提案的索引号)。struct Proposal
:提案,其属性包括bytes32 name
(名称)和uint voteCount
(已获得的票数)。
需要注意,address
类型记录了一个以太坊账户的地址。address
可看作一个数值类型,但也包括一些与以太币相关的方法,如查询余额 <address>.balance
、向该地址转账 <address>.transfer(uint256 amount)
等。
状态变量
合约中的状态变量会长期保存在区块链中。通过调用合约中的函数,这些状态变量可以被读取和改写。
本例中定义了 3 个状态变量:chairperson
、voters
、proposals
。
address public chairperson
:投票发起人,类型为address
。mapping(address => Voter) public voters
:所有投票人,类型为address
到Voter
的映射。Proposal[] public proposals
:所有提案,类型为动态大小的Proposal
数组。
3 个状态变量都使用了 public
关键字,使得变量可以被外部访问(即通过消息调用)。事实上,编译器会自动为 public
的变量创建同名的 getter 函数,供外部直接读取。
状态变量还可设置为 internal
或 private
。internal
的状态变量只能被该合约和继承该合约的子合约访问,private
的状态变量只能被该合约访问。状态变量默认为 internal
。
将上述关键状态信息设置为 public
能够增加投票的公平性和透明性。
函数
合约中的函数用于处理业务逻辑。函数的可见性默认为 public
,即可以从内部或外部调用,是合约的对外接口。函数可见性也可设置为 external
、internal
和 private
。
本例实现了 6 个 public
函数,可看作 6 个对外接口,功能分别如下。
创建投票
函数 function Ballot(bytes32[] proposalNames)
用于创建一个新的投票。
所有提案的名称通过参数 bytes32[] proposalNames
传入,逐个记录到状态变量 proposals
中。同时用 msg.sender
获取当前调用消息的发送者的地址,记录为投票发起人 chairperson
,该发起人投票权重设为 1。
赋予投票权
函数 function giveRightToVote(address voter)
实现给投票人赋予投票权。
该函数给 address voter
赋予投票权,即将 voter
的投票权重设为 1,存入 voters
状态变量。
这个函数只有投票发起人 chairperson
可以调用。这里用到了 require((msg.sender == chairperson) && !voters[voter].voted)
函数。如果 require
中表达式结果为 false
,这次调用会中止,且回滚所有状态和以太币余额的改变到调用前。但已消耗的 Gas 不会返还。
委托投票权
函数 function delegate(address to)
把投票委托给其他投票人。
其中,用 voters[msg.sender]
获取委托人,即此次调用的发起人。用 require
确保发起人没有投过票,且不是委托给自己。由于被委托人也可能已将投票委托出去,所以接下来,用 while
循环查找最终的投票代表。找到后,如果投票代表已投票,则将委托人的权重加到所投的提案上;如果投票代表还未投票,则将委托人的权重加到代表的权重上。
该函数使用了 while
循环,这里合约编写者需要十分谨慎,防止调用者消耗过多 Gas,甚至出现死循环。
进行投票
函数 function vote(uint proposal)
实现投票过程。
其中,用 voters[msg.sender]
获取投票人,即此次调用的发起人。接下来检查是否是重复投票,如果不是,进行投票后相关状态变量的更新。
查询获胜提案
函数 function winningProposal() constant returns (uint winningProposal)
将返回获胜提案的索引号。
这里,returns (uint winningProposal)
指定了函数的返回值类型,constant
表示该函数不会改变合约状态变量的值。
函数通过遍历所有提案进行记票,得到获胜提案。
查询获胜者名称
函数 function winnerName() constant returns (bytes32 winnerName)
实现返回获胜者的名称。
这里采用内部调用 winningProposal()
函数的方式获得获胜提案。如果需要采用外部调用,则需要写为 this.winningProposal()
。
本章小结
以太坊项目将区块链技术在数字货币的基础上进行了延伸,提出打造更为通用的智能合约平台的宏大构想,并基于开源技术构建了以太坊为核心的开源生态系统。
本章内容介绍了以太坊的相关知识,包括核心概念、设计、工具,以及客户端的安装、智能合约的使用和编写等。
比照比特币项目,读者通过学习可以掌握以太坊的相关改进设计,并学习智能合约的编写。实际上,智能合约并不是一个新兴概念,但区块链技术的出现为智能合约的“代码即律法”提供提供了信任基础和实施架构。通过引入智能合约,区块链技术释放了支持更多应用领域的巨大潜力。