精通以太坊7:智能合约与Solidity
以太坊包含两种类型账号:
外部账号(EOA)和合约账号
外部账号由用户通过独立于以太坊平台的钱包软件控制。与之相反,合约账户由他的代码(也是就是智能合约)来控制,这些代码运行在以太坊虚拟机(EVM)上。
外部账户就是一个账户,没有任何代码和状态存储与之关联,而合约账户则有其代码和数据状态存储。外部账户被交易控制,交易由来自现实世界中的私钥所创建并签名,并且是与协议独立的;合约账户并没有私钥,而是由他的智能合约代码进行预先控制。这类账户都是通过以太坊地址表示的。
接下来我们学习合约账户,以及控制这类账户的程序代码—智能合约
7.1什么是智能合约
智能合约这个术语已经在过去数十年被用于描述各种不同领域的事物。在20世纪90年代,密码学家Nick Szabo定义了这个术语,并把它定义为“一系列承诺,通过数字化的形式,包括一组协议和在协议中的各方执行的其他承诺”。
从那时起,智能合约这个概念发生了变化,特别是从2009年开始,以比特币为代表的去中心化区块链平台出现之后。在以太坊的上下文中,这个概念实际上有一些用词不当,以太坊的智能合约既不智能,也没有法律上的合约效力,但是这个术语已经广为流传。在本书中,我们使用智能合约来指代那些不可改变的计算机程序,以确定性方式运行在以太坊的虚拟机上,也就是我们常说的以太坊去中心化世界计算机。
我们来注意拆解定义:
计算机程序:
简单地说,智能合约就是计算机程序。在这个上下文中,合约这个词并没有任何法律上相关的含义。
不该改变
一旦部署之后,智能合约的代码就不能被更改。不像是传统的软件,更改智能合约的唯一办法就是部署一个新的实例。
确性性的
对于触发智能合约执行的交易上下文,或执行时的以太坊区块链状态,智能合约执行结果的输出对于每一个运行或调用它的人来说都是一样的,
以太坊虚拟机上下文
智能合约运行在一个非常有限的执行环境中。它们可以访问自己的状态,调用合约交易的上下文信息,以及有关最近区块的信息。
去中心化的世界计算机
EVM作为每一个以太坊节点的本地实例运行,但是因为所有EVM都是运行在相同的初始状态,并且会输出完全相同的最终状态,所以整个系统就像是一台世界计算机。
7.2以太坊高级编程语言
EVM是一个运行字节码这种特殊形式机器码的虚拟机,类似运行x86_64指令集的计算机CPU。
显然,可以直接使用字节码开发智能合约,但是EVM的字节码对于程序员来说非常难读和难懂。因此,大多数以太坊开发者使用高级编程语言编写智能合约,然后通过编译器转化为EVM字节码。
尽管任何高级编程语言都可以用来编写智能合约,但是让这些编程语言去兼容EVM的字节码却是一件苦差事,而且往往会无功而返。智能合约运行在一个高度隔离并且极其简单的执行环境(EVM)中,大多数常见的用户界面、操作系统接口和硬件接口在EVM环境中都不复存在。另外,跟EVM相关的一系列系统参数和函数也需要在编程语言中有所体现和支持。因此,从头开始开发一款全新的编程语言反而可能是好办法,因为这样不会受到通用编程语言的种种限制,而且更适合专用于编写智能合约。因此,一系列专门用于编写智能合约的编程语言开始涌现。以太坊有若干种智能合约编程语言和对应这些编程语言的编译器,用于生成可供EVM执行的字节码。
总体而言,编程语言可以分为两大类别:声明式的和指令式的,也对应称为函数式的和过程式的。在声明式的编程语言中,我们通过编写函数来表示程序的逻辑,但是不体现出程序的执行过程。声明式编程语言用于编写那些没有副作用的程序,就是对函数之外的状态没有修改的程序。声明式编程语言包括Haskell和SQL。指令式编程语言与之相反,是指程序员编写一组包含了逻辑和执行流程的指令。指令式编程语言包括C++和Java。有些编程语言是混合式的,意味着尽管这些语言鼓励使用声明式,但是也可以用来表述指令式的程序片段。这些混合式的编程语言包括Lisp、JavaScript和Python。简而言之,指令式的语言可以用来编写声明式的代码,但是会产生一些不够优雅的代码。相比之下,单纯的声明式语言不能用来编写指令式代码,因为在纯声明式语言中,并没有变量这个概念。
尽管指令式程序代码更容易编写和阅读,大多数程序员也都在使用,但是却很难用于编写那些严格按部就班执行的代码。程序的任何部分都有可能改变状态,这使得我们很难推断程序的执行,并为非预期的副作用和错误引入了许多机会。声明式程序也许更难编写,但避免了副作用,从而更容易理解(和控制)程序的行为方式——程序的每个部分都是相互独立的,降低了理解程序的难度。
智能合约为程序员设定了一个很高的门槛:如果有bug,可能会损失大量的金钱。因此,编写智能合约就需要极力避免任何可能的副作用。为此,你必须清楚地了解程序的预期行为。可见,声明式编程语言在智能合约中的作用要大于在通用软件中的作用。然而,正如你将在下面看到的,智能合约(Solidity)最为广泛使用的语言却是指令式的。程序员亦凡人,他们也会拒绝改变!目前可用于智能合约编写的高级语言如下(按照出现的先后顺序):
LLL
一种函数式(声明式)编程语言,语法类似LISP,这是首款可以运行在以太坊上编写智能合约的编程语言由Gavin Wood编写,但是现在很少人用
Serpent
一种过程式(指令式)编程语言,语法类似Python。也可以用来编写函数式(声明式)代码,尽管它并不是完全没有副作用。
Solidity
一种过程式的编程语言,语法类似 JAVAsCript c++,java
这是最流行也是最常用的以太坊智能合约编程语言。
Vyper
一种最近推出的编程语言,语法类似Serpent和Python。目标在于达到比Serpent更加纯函数式的类Python语言,但并不是为了替代Serpent。
Bamboo
一种最近推出的编程语言,受Erlang启发,引入了显式状态转换,并去掉了递归式的循环。旨在减少副作用并提升可审计性。非常新,还未被广泛使用。
7.3使用Solidity编写智能合约
Solidity由Gavin Wood博士创建,作为一种专门编写智能合约的语言,支持在以太坊世界计算机的去中心化环境中直接执行。由于此编程语言具备普遍性,因此最终也被用于在其他几个区块链平台上编码智能合约。它由ChristianReitiwessner开发,然后由Alex Beregszaszi、Liana Husikyan、Yoichi Hirai和几位前以太坊核心贡献者维护。现在,作为GitHub上的独立项目继续被开发并维护(https://github.com/ethereum/solidity)。
Solidity项目的产出主要是Solidity编译器(solc),它用于把Solidity语言编写的代码编译为字节码。这个项目也管理以太坊智能合约的应用程序二进制接口(ABI)标准,这个概念我们会在本章稍后的内容中讨论。每一个版本的Solidity编译器都与它相应版本的编程语言一一对应。
7.4选择Solidity版本
Solidity的版本模型采用语义化版本(https://semver.org/),也就是说,通过MAJOR. MINOR.PATCH三组数字的方式来制定版本。主版本号用于主要的且不后向兼容的改变,次版本号代表两次大版本之间渐进式的后向兼容的改变,最后一位补丁号表示用于bug修复的后向兼容的版本。在写作本书时,Solidity的版本是0.4.24。其中,主版本号0(用于项目的初始开发)的含义有所不同:它表示任何东西都可能随时更改。在实践中,Solidity将次版本号视为主版本号,将补丁号视为次版本号。因此,在0.4.24中,我们认为4是主版本号,24是次版本号。Solidty的0.5版本即将发布。如我们在第2章的例子中提到的,Solidity程序可以包含一个编译指令,用于指定当前代码所兼容的用于编译合约的最低和最高版本。因为Solidity正处在高速发展的过程中,因此建议总是使用最新的版本。
7.5下载和安装
下载和安装Solidity的方式有很多种,可以使用二进制发布包,也可以直接利用源代码进行编译。你可以在Solidity的文档中看到详细的介绍:https://solidity.readthedocs.io/en/latest/installing-solidity.html。使用apt包管理器在Ubuntu/Debian操作系统上安装最新的Solidity二进制发布包:
7.6开发环境
你可以使用任何文本编辑器结合命令行的solc进行Solidity程序开发。但是,你会发现有些专门用于软件开发的文本编辑器,比如Emacs、Vim和Atom,它们提供了类似语法高亮和宏等高级功能,这些都会让Solidity的开发变得更容易。还有基于Web页面的开发环境,例如RemixIDE(https://remix.ethereum.org)和EthFiddle(https://ethfiddle.com)。使用这些工具会提升你的效率。最终,Solidity的程序代码只是一些纯文本文件,这些功能复杂的编辑器和开发环境会让编写代码的过程变得轻松,其实程序员只需要一个文本编辑器,比如Linux/Unix中的nano,或者MacOS中的TextEdit,甚至Windows中的NotePad。把程序源代码文件保存为.sol的扩展名,Solidity的编译器就能够识别并进行编译操作
7.7编写一个简单的solidity程序
。首次构建Faucet时,我们使用Remix IDE来编译和部署合约。在这一节,我们尝试重新编写并且改进和优化Faucet程序。之前的代码是这样的:代码7-1:Faucet.sol:实现Faucet合约的Solidity代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nBy5dXX5-1585495621353)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585493808612.png)]
7.8使用Solidity编译器进行编译
现在,我们将在命令行使用Solidity编译器直接编译合约。Solidity编译器solc提供了诸多设置选项,可以使用–help参数进行查看。我们使用solc命令–bin和–optimize来生成和优化合约的二进制代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0rogb2u3-1585495621354)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585493970095.png)]
solc编译后生成十六进制的合约字节码,可以提交给以太坊区块链。
7.9以太坊合约的应用程序二进制接口
在计算机软件中,应用程序二进制接口(Application Binary Interface, ABI)是指两个程序模块之间的接口,通常,一个在操作系统层面,另外一个在用户程序层面。ABI定义了数据结构和函数如何在机器指令中被访问。需要注意,这并不是我们常说的API, API定义了高级的、供程序员阅读和使用的源代码接口。ABI是向机器指令层面编码和解码并传送数据的主要方式。
在以太坊中,ABI用来编码合约中对EVM的调用和从交易中获取数据的调用。ABI的目的是定义合约中哪一个函数可以被调用,并且描述这个函数接收的参数和返回的数据。
合约ABI使用JSON格式表示,其中包含描述合约中函数和事件的数组(本章后面会具体讨论)。在JSON中,有关函数的属性描述字段包括type、name、inputs、outputs、constant和payable。有关事件的属性描述字段包括type、name、inputs和anonymous。
我们可以使用solc命令行编译器来生成Faucet.sol合约的ABI
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SFuM20QR-1585495621355)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585494242493.png)]
如你所见,编译器对Faucet.sol合约中定义的两个函数生成了JSON格式的描述信息。当合约部署以后,这个JSON对象可以被任何希望访问Faucet.sol合约的程序所解析。使用ABI、钱包或者DApp浏览器可以获得正确的调用参数和格式,构建调用Faucet.sol合约的以太坊交易。例如,钱包软件在调用withdraw函数时,需要通过ABI知道,这个调用需要提供一个uint256类型的变量,变量的名称是withdraw_amount。然后钱包软件就会提示用户输入这个参数,接着创建一个以太坊交易,调用合约的withdraw函数。
应用与合约的交互完全依赖于ABI和应用实例部署的以太坊地址。
7.10选择Solidity编译器和语言版本
在之前的代码中,我们演示了Faucet合约通过Solidity 0.4.21版本的编译。但是,如果我们使用不用版本的编译器会发生什么?Solidity编程语言本身仍旧处在快速变化的过程中,因此可能会出现无法预料的问题。我们的合约相对比较简单,但是如果合约代码中使用了只在Solidity 0.4.19中提供的功能,而我们却尝试使用0.4.18进行编译,会发生什么?
为了解决这样的问题,Solidity提供了一个叫作编译器侦测的功能,使用版本编译指令来告诉编译器这段程序所需要的编译器版本号。我们来看一个例子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nPiG8LkR-1585495621356)(C:\Users\xiaoweifeng\AppData\Roaming\Typora\typora-user-images\1585494445132.png)]
Solidity编译器会读取这个版本要求,如果当前编译器的版本与要求不符,就会提示报错。在我们的例子中,版本编译指令指出程序需要编译器的版本不低于0.4.19。“^”这个符号表示,我们的程序可以跟任何次版本号高于0.4.19(比如0.4.20)的编译器兼容,但无法与0.5.0这类主版本号的编译器兼容。版本编译指令不会被编译到EVM字节码,只是用在编译之前的兼容性检查。
让我们在Faucet合约中加入编译器版本要求。我们把新的合约命名为Faucet2.sol,用于跟踪后续例子中做出的修改:
.4.20)的编译器兼容,但无法与0.5.0这类主版本号的编译器兼容。版本编译指令不会被编译到EVM字节码,只是用在编译之前的兼容性检查。
让我们在Faucet合约中加入编译器版本要求。我们把新的合约命名为Faucet2.sol,用于跟踪后续例子中做出的修改:
添加编译器版本要求是一种最佳实践,这样能够避免编译器和代码版本不兼容造成的问题。本章我们会接着介绍其他最佳实践,继续改进Faucet合约