以太坊智能合约编程简单教程(全)
有些人说以太坊太难对付,于是我们(译注:指Consensys, 下同)写了这篇文章来帮助大家学习如何利用以太坊编写智能合约和应用。这里所用到的工具,钱包,应用程序以及整个生态系统仍处于开发状态,它们将来会更好用!
- 第一部分概述,讨论了关键概念,几大以太坊客户端以及写智能合约用到的编程语言。
- 第二部分讨论了总体的工作流程,以及目前流行的一些DApp框架和工具。
- 第三部分主要关于编程,我们将学习如何使用Truffle来为智能合约编写测试和构建DApp。
第一部分. 概述
如果你对诸如比特币以及其工作原理等密码学货币的概念完全陌生,我们建议你先看看Andreas Antonopoulos所著的Bitcoin Book的头几章,然后读一下以太坊白皮书。(译注:以太坊白皮书中文版请看 http://ethfans.org/posts/ethereum-whitepaper)
如果你觉得白皮书中的章节太晦涩,也可以直接动手来熟悉以太坊。在以太坊上做开发并不要求你理解所有那些“密码经济计算机科学”(crypto economic computer science),而白皮书的大部分是关于以太坊想对于比特币架构上的改进。
新手教程
ethereum.org提供了官方的新手入门教程,以及一个代币合约和众筹合约的教程。合约语言Solidity也有官方文档。学习智能合约的另一份不错的资料(也是我的入门资料)是dappsForBeginners,不过现在可能有些过时了。
这篇文章的目的是成为上述资料的补充,同时介绍一些基本的开发者工具,使入门以太坊,智能合约以及构建DApps(decentralized apps, 分布式应用)更加容易。我会试图按照我自己(依然是新手)的理解来解释工作流程中的每一步是在做什么,我也得到了ConsenSys酷酷的开发者们的许多帮助。
基本概念
了解这些名词是一个不错的开始:
公钥加密系统。 Alice有一把公钥和一把私钥。她可以用她的私钥创建数字签名,而Bob可以用她的公钥来验证这个签名确实是用Alice的私钥创建的,也就是说,确实是Alice的签名。当你创建一个以太坊或者比特币钱包的时候,那长长的0xdf...5f
地址实质上是个公钥,对应的私钥保存某处。类似于Coinbase的在线钱包可以帮你保管私钥,你也可以自己保管。如果你弄丢了存有资金的钱包的私钥,你就等于永远失去了那笔资金,因此你最好对私钥做好备份。过来人表示:通过踩坑学习到这一点是非常痛苦的...
点对点网络。 就像BitTorrent, 以太坊分布式网络中的所有节点都地位平等,没有中心服务器。(未来会有半中心化的混合型服务出现为用户和开发者提供方便,这我们后面会讲到。)
区块链。 区块链就像是一个全球唯一的帐簿,或者说是数据库,记录了网络中所有交易历史。
以太坊虚拟机(EVM)。 它让你能在以太坊上写出更强大的程序(比特币上也可以写脚本程序)。它有时也用来指以太坊区块链,负责执行智能合约以及一切。
节点。 你可以运行节点,通过它读写以太坊区块链,也即使用以太坊虚拟机。完全节点需要下载整个区块链。轻节点仍在开发中。
矿工。 挖矿,也就是处理区块链上的区块的节点。这个网页可以看到当前活跃的一部分以太坊矿工:stats.ethdev.com。
工作量证明。 矿工们总是在竞争解决一些数学问题。第一个解出答案的(算出下一个区块)将获得以太币作为奖励。然后所有节点都更新自己的区块链。所有想要算出下一个区块的矿工都有与其他节点保持同步,并且维护同一个区块链的动力,因此整个网络总是能达成共识。(注意:以太坊正计划转向没有矿工的权益证明系统(POS),不过那不在本文讨论范围之内。)
以太币。 缩写ETH。一种你可以购买和使用的真正的数字货币。这里是可以交易以太币的其中一家交易所的走势图。在写这篇文章的时候,1个以太币价值65美分。
Gas. (汽油) 在以太坊上执行程序以及保存数据都要消耗一定量的以太币,Gas是以太币转换而成。这个机制用来保证效率。
DApp. 以太坊社区把基于智能合约的应用称为去中心化的应用程序(Decentralized App)。DApp的目标是(或者应该是)让你的智能合约有一个友好的界面,外加一些额外的东西,例如IPFS(可以存储和读取数据的去中心化网络,不是出自以太坊团队但有类似的精神)。DApp可以跑在一台能与以太坊节点交互的中心化服务器上,也可以跑在任意一个以太坊平等节点上。(花一分钟思考一下:与一般的网站不同,DApp不能跑在普通的服务器上。他们需要提交交易到区块链并且从区块链而不是中心化数据库读取重要数据。相对于典型的用户登录系统,用户有可能被表示成一个钱包地址而其它用户数据保存在本地。许多事情都会与目前的web应用有不同架构。)
如果想看看从另一个新手视角怎么理解这些概念,请读Just Enough Bitcoin for Ethereum。
以太坊客户端,智能合约语言
编写和部署智能合约并不要求你运行一个以太坊节点。下面有列出基于浏览器的IDE和API。但如果是为了学习的话,还是应该运行一个以太坊节点,以便理解其中的基本组件,何况运行节点也不难。
运行以太坊节点可用的客户端
以太坊有许多不同语言的客户端实现(即多种与以太坊网络交互的方法),包括C++, Go, Python, Java, Haskell等等。为什么需要这么多实现?不同的实现能满足不同的需求(例如Haskell实现的目标是可以被数学验证),能使以太坊更加安全,能丰富整个生态系统。
在写作本文时,我使用的是Go语言实现的客户端geth (go-ethereum),其他时候还会使用一个叫testrpc的工具, 它使用了Python客户端pyethereum。后面的例子会用到这些工具。
注: 我曾经使用过C++的客户端,现在仍然在用其中的ethminer组件和geth配合挖矿,因此这些不同的组件是可以一起工作的。
关于挖矿:挖矿很有趣,有点像精心照料你的室内盆栽,同时又是一种了解整个系统的方法。虽然以太币现在的价格可能连电费都补不齐,但以后谁知道呢。人们正在创造许多酷酷的DApp, 可能会让以太坊越来越流行。
交互式控制台。 客户端运行起来后,你就可以同步区块链,建立钱包,收发以太币了。使用geth的一种方式是通过Javascript控制台(JavaScript console, 类似你在chrome浏览器里面按F12出来的那个,只不过是跑在终端里)。此外还可以使用类似cURL的命令通过JSON RPC来与客户端交互。本文的目标是带大家过一边DApp开发的流程,因此这块就不多说了。但是我们应该记住这些命令行工具是调试,配置节点,以及使用钱包的利器。
在测试网络运行节点。 如果你在正式网络运行geth客户端,下载整个区块链与网络同步会需要相当时间。(你可以通过比较节点日志中打印的最后一个块号和stats.ethdev.com上列出的最新块来确定是否已经同步。) 另一个问题是在正式网络上跑智能合约需要实实在在的以太币。在测试网络上运行节点的话就没有这个问题。此时也不需要同步整个区块链,创建一个自己的私有链就勾了,对于开发来说更省时间。
testrpc. 用geth可以创建一个测试网络,另一种更快的创建测试网络的方法是使用testrpc. Testrpc可以在启动时帮你创建一堆存有资金的测试账户。它的运行速度也更快因此更适合开发和测试。你可以从testrpc起步,然后随着合约慢慢成型,转移到geth创建的测试网络上 - 启动方法很简单,只需要指定一个networkid:geth --networkid "12345"
。这里是testrpc的代码仓库,下文我们还会再讲到它。
接下来我们来谈谈可用的编程语言,之后就可以开始真正的编程了。
写智能合约用的编程语言
用Solidity就好。 要写智能合约有好几种语言可选:有点类似Javascript的Solidity, 文件扩展名是.sol
. 和Python接近的Serpent, 文件名以.se
结尾。还有类似Lisp的LLL。Serpent曾经流行过一段时间,但现在最流行而且最稳定的要算是Solidity了,因此用Solidity就好。听说你喜欢Python? 用Solidity。
solc编译器。 用Solidity写好智能合约之后,需要用solc来编译。它是一个来自C++客户端实现的组件(又一次,不同的实现产生互补),这里是安装方法。如果你不想安装solc也可以直接使用基于浏览器的编译器,例如Solidity real-time compiler或者Cosmo。后文有关编程的部分会假设你安装了solc。
注意:以太坊正处于积极的开发中,有时候新的版本之间会有不同步。确认你使用的是最新的dev版本,或者稳定版本。如果遇到问题可以去以太坊项目对应的Gitter聊天室或者forums.ethereum.org上问问其他人在用什么版本。
web3.js API. 当Solidity合约编译好并且发送到网络上之后,你可以使用以太坊的web3.js JavaScript API来调用它,构建能与之交互的web应用。
以上就是在以太坊上编写智能合约和构建与之交互的DApp所需的基本工具。
第二部分. DApp框架,工具以及工作流程
DApp开发框架
虽然有上文提到的工具就可以进行开发了,但是使用社区大神们创造的框架会让开发更容易。
Truffle and Embark. 是Truffle把我领进了门。在Truffle出现之前的那个夏天,我目睹了一帮有天分的学生是如何不眠不休的参加一个hackathon(编程马拉松)活动的,虽然结果相当不错,但我还是吓到了。然后Truffle出现了,帮你处理掉大量无关紧要的小事情,让你可以迅速进入写代码-编译-部署-测试-打包DApp这个流程。另外一个相似的DApp构建与测试框架是Embark。我只用过Truffle, 但是两个阵营都拥有不少DApp大神。
Meteor. 许多DApp开发者使用的另一套开发栈由web3.js和Meteor组成,Meteor是一套通用webapp开发框架(ethereum-meteor-wallet项目提供了一个很棒的入门实例,而SilentCiero正在构建大量Meteor与web3.js和DApp集成的模板)。我下载并运行过一些不错的DApp是以这种方式构造的。在11月9日至13日的以太坊开发者大会ÐΞVCON1上将有一些有趣的讨论,是关于使用这些工具构建DApp以及相关最佳实践的(会议将会在YouTube上直播)。
APIs. BlockApps.net打算提供一套RESTful API给DApp使用以免去开发者运行本地节点的麻烦,这个中心化服务是基于以太坊Haskell实现的。这与DApp的去中心化模型背道而驰,但是在本地无法运行以太坊节点的场合非常有用,比如在你希望只有浏览器或者使用移动设备的用户也能使用你的DApp的时候。BlockApps提供了一个命令行工具bloc,注册一个开发者帐号之后就可以使用。
许多人担心需要运行以太坊节点才能使用DApp的话会把用户吓跑,其实包括BlockApps在内的许多工具都能解决这个问题。Metamask允许你在浏览器里面使用以太坊的功能而无需节点,以太坊官方提供的AlethZero或者AlethOne是正在开发中有易用界面的客户端,ConsenSys正在打造一个轻钱包LightWallet,这些工具都会让DApp的使用变得更容易。轻客户端和水平分片(sharding)也在计划和开发之中。这是一个能进化出混合架构的P2P生态系统。
智能合约集成开发环境 (IDE)
IDE. 以太坊官方出品了用来编写智能合约的Mix IDE,我还没用过但会尽快一试。
基于浏览器的IDE. Solidity real-time compiler和Cosmo都可以让你快速开始在浏览器中编写智能合约。你甚至可以让这些工具使用你的本地节点,只要让本地节点开一个端口(注意安全!这些工具站点必须可信,而且千万不要把你的全部身家放在这样一个本地节点里面!Cosmo UI上有如何使用geth做到这一点的指引)。在你的智能合约调试通过之后,可以用开发框架来给它添加用户界面和打包成DApp,这正是Truffle的工作,后面的编程章节会有详细讲解。
Ether.Camp正在开发另一个强大的企业级浏览器IDE。他们的IDE将支持沙盒测试网络,自动生成用于测试的用户界面(取代后文将展示的手动编写测试),以及一个测试交易浏览器test.ether.camp。当你的合约准备正式上线之前,使用他们的测试网络会是确保你的智能合约在一个接近真实的环境工作正常的好方法。他们也为正式网络提供了一个交易浏览器frontier.ether.camp,上面可以看到每一笔交易的细节。在本文写作时Ether.Camp的IDE还只能通过邀请注册,预计很快会正式发布。
合约和Dapp示例。 在Github上搜索DApp仓库和.sol文件可以看到进行中的有趣东西。这里有一个DApp大列表:dapps.ethercasts.com,不过其中一些项目已经过时。Ether.fund/contracts上有一些Solidity和Serpent写的合约示例,但是不清楚这些例子有没有经过测试或者正确性验证。11月12日的开发者大会ÐΞVCON1将会有一整天的DApp主题演讲。
部署智能合约的流程
流程如下:
- 启动一个以太坊节点 (例如geth或者testrpc)。
- 使用solc*编译*智能合约。 => 获得二进制代码。
- 将编译好的合约部署到网络。(这一步会消耗以太币,还需要使用你的节点的默认地址或者指定地址来给合约签名。) => 获得合约的区块链地址和ABI(合约接口的JSON表示,包括变量,事件和可以调用的方法)。(译注:作者在这里把ABI与合约接口弄混了。ABI是合约接口的二进制表示。)
- 用web3.js提供的JavaScript API来调用合约。(根据调用的类型有可能会消耗以太币。)
下图详细描绘了这个流程:
你的DApp可以给用户提供一个界面先部署所需合约再使用之(如图1到4步),也可以假设合约已经部署了(常见方法),直接从使用合约(如图第6步)的界面开始。
第三部分. 编程
在Truffle中进行测试
Truffle用来做智能合约的测试驱动开发(TDD)非常棒,我强烈推荐你在学习中使用它。它也是学习使用JavaScript Promise的一个好途径,例如deferred和异步调用。Promise机制有点像是说“做这件事,如果结果是这样,做甲,如果结果是那样,做乙... 与此同时不要在那儿干等着结果返回,行不?”。Truffle使用了包装web3.js的一个JS Promise框架Pudding(因此它为为你安装web3.js)。(译注:Promise是流行于JavaScript社区中的一种异步调用模式。它很好的封装了异步调用,使其能够灵活组合,而不会陷入callback hell.)
Transaction times. Promise对于DApp非常有用,因为交易写入以太坊区块链需要大约12-15秒的时间。即使在测试网络上看起来没有那么慢,在正式网络上却可能会要更长的时间(例如你的交易可能用光了Gas,或者被写入了一个孤儿块)。
下面让我们给一个简单的智能合约写测试用例吧。
使用Truffle
首先确保你 1.安装好了solc以及 2.testrpc。(testrpc需要Python和pip。如果你是Python新手,你可能需要用virtualenv来安装,这可以将Python程序库安装在一个独立的环境中。)
接下来安装 3.Truffle(你可以使用NodeJS"s npm来安装:npm install -g truffle
, -g
开关可能会需要sudo)。安装好之后,在命令行中输入truffle list
来验证安装成功。然后创建一个新的项目目录(我把它命名为"conference"),进入这个目录,运行truffle init
。该命令会建立如下的目录结构:
现在让我们在另一个终端里通过执行testrpc
来启动一个节点(你也可以用geth):
回到之前的终端中,输入truffle deploy
。这条命令会部署之前truffle init
产生的模板合约到网络上。任何你可能遇到的错误信息都会在testrpc的终端或者执行truffle的终端中输出。
在开发过程中你随时可以使用truffle compile
命令来确认你的合约可以正常编译(或者使用solc YourContract.sol
),truffle deploy
来编译和部署合约,最后是truffle test
来运行智能合约的测试用例。
第一个合约
下面是一个针对会议的智能合约,通过它参会者可以买票,组织者可以设置参会人数上限,以及退款策略。本文涉及的所有代码都可以在这个代码仓库找到。
contract Conference {
address public organizer;
mapping (address => uint) public registrantsPaid;
uint public numRegistrants;
uint public quota;
event Deposit(address _from, uint _amount); // so you can log these events
event Refund(address _to, uint _amount);
function Conference() { // Constructor
organizer = msg.sender;
quota = 500;
numRegistrants = 0;
}
function buyTicket() public returns (bool success) {
if (numRegistrants >= quota) { return false; }
registrantsPaid[msg.sender] = msg.value;
numRegistrants++;
Deposit(msg.sender, msg.value);
return true;
}
function changeQuota(uint newquota) public {
if (msg.sender != organizer) { return; }
quota = newquota;
}
function refundTicket(address recipient, uint amount) public {
if (msg.sender != organizer) { return; }
if (registrantsPaid[recipient] == amount) {
address myAddress = this;
if (myAddress.balance >= amount) {
recipient.send(amount);
registrantsPaid[recipient] = 0;
numRegistrants--;
Refund(recipient, amount);
}
}
}
function destroy() { // so funds not locked in contract forever
if (msg.sender == organizer) {
suicide(organizer); // send funds to organizer
}
}
}
接下来让我们部署这个合约。(注意:本文写作时我使用的是Mac OS X 10.10.5, solc 0.1.3+ (通过brew安装),Truffle v0.2.3, testrpc v0.1.18 (使用venv))
部署合约
(译注:图中步骤翻译如下:)
使用truffle部署智能合约的步骤:
1. truffle init
(在新目录中) => 创建truffle项目目录结构
2. 编写合约代码,保存到contracts/YourContractName.sol
文件。
3. 把合约名字加到config/app.json
的"contracts"部分。
4. 启动以太坊节点(例如在另一个终端里面运行testrpc
)。
5. truffle deploy
(在truffle项目目录中)
添加一个智能合约。 在truffle init
执行后或是一个现有的项目目录中,复制粘帖上面的会议合约到contracts/Conference.sol
文件中。然后打开config/app.json
文件,把"Conference"加入"deploy"数组中。
启动testrpc。 在另一个终端中启动testrpc
。
编译或部署。 执行truffle compile
看一下合约是否能成功编译,或者直接truffle deploy
一步完成编译和部署。这条命令会把部署好的合约的地址和ABI(应用接口)加入到配置文件中,这样之后的truffle test
和truffle build
步骤可以使用这些信息。
出错了? 编译是否成功了?记住,错误信息即可能出现在testrpc终端也可能出现在truffle终端。
重启节点后记得重新部署! 如果你停止了testrpc节点,下一次使用任何合约之前切记使用truffle deploy
重新部署。testrpc在每一次重启之后都会回到完全空白的状态。
合约代码解读
让我们从智能合约头部的变量声明开始:
address public organizer;
mapping (address => uint) public registrantsPaid;
uint public numRegistrants;
uint public quota;
address. 地址类型。第一个变量是会议组织者的钱包地址。这个地址会在合约的构造函数function Conference()
中被赋值。很多时候也称呼这种地址为"owner"(所有人)。
uint. 无符号整型。区块链上的存储空间很紧张,保持数据尽可能的小。
public. 这个关键字表明变量可以被合约之外的对象使用。private
修饰符则表示变量只能被本合约(或者衍生合约)内的对象使用。如果你想要在测试中通过web3.js使用合约中的某个变量,记得把它声明为public
。
Mapping或数组。(译注:Mapping类似Hash, Directory等数据类型,不做翻译。)在Solidity加入数组类型之前,大家都使用类似mapping (address => uint)
的Mapping类型。这个声明也可以写作address registrantsPaid[]
,不过Mapping的存储占用更小(smaller footprint)。这个Mapping变量会用来保存参加者(用他们的钱包地址表示)的付款数量以便在退款时使用。
关于地址。 你的客户端(比如testrpc或者geth)可以生成一个或多个账户/地址。testrpc启动时会显示10个可用地址:
第一个地址, accounts[0]
,是发起调用的默认地址,如果没有特别指定的话。
组织者地址 vs. 合约地址。 部署好的合约会在区块链上拥有自己的地址(与组织者拥有的是不同的地址)。在Solidity合约中可以使用this
来访问这个合约地址,正如refundTicket
函数所展示的:address myAddress = this;
Suicide, Solidity的好东西。(译注:suicide
意为"自杀", 为Solidity提供的关键字,不做翻译。)转给合约的资金会保存于合约(地址)中。最终这些资金通过destroy
函数被释放给了构造函数中设置的组织者地址。这是通过suicide(orgnizer);
这行代码实现的。没有这个,资金可能被永远锁定在合约之中(reddit上有些人就遇到过),因此如果你的合约会接受资金一定要记得在合约中使用这个方法!
如果想要模拟另一个用户或者对手方(例如你是卖家想要模拟一个买家),你可以使用可用地址数组中另外的地址。假设你要以另一个用户,accounts[1]
, 的身份来买票,可以通过from
参数设置:
conference.buyTicket({ from: accounts[1], value: some_ticket_price_integer });
函数调用可以是交易。 改变合约状态(修改变量值,添加记录,等等)的函数调用本身也是转账交易,隐式的包含了发送人和交易价值。因此web3.js的函数调用可以通过指定{ from: __, value: __ }
参数来发送以太币。在Solidity合约中,你可以通过msg.sender
和msg.value
来获取这些信息:
function buyTicket() public {
...
registrantsPaid[msg.sender] = msg.value;
...
}
事件(Event)。 可选的功能。合约中的Deposit
(充值)和Send
(发送)事件是会被记录在以太坊虚拟机日志中的数据。它们实际上没有任何作用,但是用事件(Event)把交易记录进日志是好的做法。
好了,现在让我们给这个智能合约写一个测试,来确保它能工作。
写测试
把项目目录test/
中的example.js
文件重命名为conference.js
,文件中所有的"Example"替换为"Conference"。
contract("Conference", function(accounts) {
it("should assert true", function(done) {
var conference = Conference.at(Conference.deployed_address);
assert.isTrue(true);
done(); // stops tests at this point
});
});
在项目根目录下运行truffle test
,你应该看到测试通过。在上面的测试中truffle通过Conference.deployed_address
获得合约部署在区块链上的地址。
让我们写一个测试来初始化一个新的Conference,然后检查变量都正确赋值了。将conference.js
中的测试代码替换为:
contract("Conference", function(accounts) {
it("Initial conference settings should match", function(done) {
var conference = Conference.at(Conference.deployed_address);
// same as previous example up to here
Conference.new({ from: accounts[0] })
.then(function(conference) {
conference.quota.call().then(
function(quota) {
assert.equal(quota, 500, "Quota doesn"t match!");
}).then( function() {
return conference.numRegistrants.call();
}).then( function(num) {
assert.equal(num, 0, "Registrants should be zero!");
return conference.organizer.call();
}).then( function(organizer) {
assert.equal(organizer, accounts[0], "Owner doesn"t match!");
done(); // to stop these tests earlier, move this up
}).catch(done);
}).catch(done);
});
});
构造函数。 Conference.new({ from: accounts[0] })
通过调用合约构造函数创造了一个新的Conference实例。由于不指定from
时会默认使用accounts[0]
,它其实可以被省略掉:
Conference.new({ from: accounts[0] }); // 和Conference.new()效果相同
Promise. 代码中的那些then
和return
就是Promise。它们的作用写成一个深深的嵌套调用链的话会是这样:
conference.numRegistrants.call().then(
function(num) {
assert.equal(num, 0, "Registrants should be zero!");
conference.organizer.call().then(
function(organizer) {
assert.equal(organizer, accounts[0], "Owner doesn"t match!");
}).then(
function(...))
}).then(
function(...))
// Because this would get hairy...
Promise减少嵌套,使代码变得扁平,允许调用异步返回,并且简化了表达“成功时做这个”和“失败时做那个”的语法。Web3.js通过回调函数实现异步调用,因此你不需要等到交易完成就可以继续执行前端代码。Truffle借助了用Promise封装web3.js的一个框架,叫做Pudding,这个框架本身又是基于Bluebird的,它支持Promise的高级特性。
call. 我们使用
- 上一篇:没有了
- 下一篇:没有了