智能合约
我们在 [intro] 中发现,以太坊有两种不同类型的账户:外部所有账户(EOAs)和合约账户。EOAs由以太坊以外的软件(如钱包应用程序)控制。合约帐户由在以太坊虚拟机(EVM)内运行的软件控制。两种类型的帐户都通过以太坊地址标识。在本节中,我们将讨论第二种类型,合约账户和控制它们的软件:智能合约。
什么是智能合约?
术语_smart contract_已被用于描述各种不同的事物。在二十世纪九十年代,密码学家Nick Szabo提出了这个术语,并将其定义为“一组以数字形式规定的承诺,包括各方在其他承诺中履行的协议”。自那时以来,智能合约的概念得到了发展,尤其是在2009年比特币发明引入了去中心化区块链之后。在本书中,我们使用术语“智能合约”来指代在Ethereum虚拟机环境中确定性的运行的不可变的计算机程序,该虚拟机作为一个去中心化的世界计算机而运转。
让我们拆解这个定义:
计算机程序:智能合约只是计算机程序。合约这个词在这方面没有法律意义。 不可变的:一旦部署,智能合约的代码不能改变。与传统软件不同,修改智能合约的唯一方法是部署新实例。 确定性的:智能合约的结果对于运行它的每个人来说都是一样的,包括调用它们的交易的上下文,以及执行时以太坊区块链的状态。 EVM上下文:智能合约以非常有限的执行上下文运行。他们可以访问自己的状态,调用它们的交易的上下文以及有关最新块的一些信息。 去中心化的世界计算机:EVM在每个以太坊节点上作为本地实例运行,但由于EVM的所有实例都在相同的初始状态下运行并产生相同的最终状态,因此整个系统作为单台世界计算机运行。
智能合约的生命周期
智能合约通常以高级语言编写,例如Solidity。但为了运行,必须将它们编译为EVM中运行的低级字节码(请参见 [evm])。一旦编译完成,它们就会随着转移到特殊的合约创建地址的交易被部署到以太坊区块链中。每个合约都由以太坊地址标识,该地址源于作为发起账户和随机数的函数的合约创建交易。合约的以太坊地址可以在交易中用作接收者,可将资金发送到合约或调用合约的某个功能。
重要的是,如果合约只有被交易调用时才会运行。以太坊的所有智能合约均由EOA发起的交易执行。合约可以调用另一个合约,其中又可以调用另一个合约,等等。但是这种执行链中的第一个合约必须始终由EOA的交易调用。合约永远不会“自行”运行,或“在后台运行”。在交易触发执行,直接或间接地作为合约调用链的一部分之前,合约在区块链上实际上是“休眠”的。
交易是 原子性的 atomic,无论他们调用多少合约或这些合约在被调用时执行的是什么。交易完全执行,仅在交易成功终止时记录全局状态(合约,帐户等)的任何更改。成功终止意味着程序执行时没有错误并且达到执行结束。如果交易由于错误而失败,则其所有效果(状态变化)都会“回滚”,就好像交易从未运行一样。失败的交易仍存储在区块链中,并从原始账户扣除gas成本,但对合约或账户状态没有其他影响。
合约的代码不能更改。然而合约可以被“删除”,从区块链上删除代码和它的内部状态(变量)。要删除合约,你需要执行称为 SELFDESTRUCT(以前称为 SUICIDE )的EVM操作码,该操作码将区块链中的合约移除。该操作花费“负的gas”,从而激励储存状态的释放。以这种方式删除合约不会删除合约的交易历史(过去),因为区块链本身是不可变的。但它确实会从所有未来的区块中移除合约状态。
以太坊高级语言简介
EVM是一台虚拟计算机,运行一种特殊形式的 机器代码 ,称为_EVM 字节码_,就像你的计算机CPU运行机器代码x86_64一样。我们将在 [evm] 中更详细地检查EVM的操作和语言。在本节中,我们将介绍如何编写智能合约以在EVM上运行。
虽然可以直接在字节码中编写智能合约。EVM字节码非常笨重,程序员难以阅读和理解。相反,大多数以太坊开发人员使用高级符号语言编写程序和编译器,将它们转换为字节码。
虽然任何高级语言都可以用来编写智能合约,但这是一项非常繁琐的工作。智能合约在高度约束和简约的执行环境(EVM)中运行,几乎所有通常的用户界面,操作系统界面和硬件界面都是缺失的。从头开始构建一个简约的智能合约语言要比限制通用语言并使其适用于编写智能合约更容易。因此,为编程智能合约出现了一些专用语言。以太坊有几种这样的语言,以及产生EVM可执行字节码所需的编译器。
一般来说,编程语言可以分为两种广泛的编程范式:分别是声明式和命令式,也分别称为“函数式”和“过程式”。在声明式编程中,我们编写的函数表示程序的 逻辑 logic,而不是 流程 flow。声明式编程用于创建没有 副作用 side effects 的程序,这意味着在函数之外没有状态变化。声明式编程语言包括Haskell,SQL和HTML等。相反,命令式编程就是程序员编写一套程序的逻辑和流程结合在一起的程序。命令式编程语言包括例如BASIC,C,C++和Java。有些语言是“混合”的,这意味着他们鼓励声明式编程,但也可以用来表达一个必要的编程范式。这样的混合体包括Lisp,Erlang,Prolog,JavaScript和Python。一般来说,任何命令式语言都可以用来在声明式的范式中编写,但它通常会导致不雅的代码。相比之下,纯粹的声明式语言不能用来写入一个命令式的范例。在纯粹的声明式语言中,没有“变量”。
虽然命令式编程更易于编写和读取,并且程序员更常用,但编写按预期方式 准确 执行的程序可能非常困难。程序的任何部分改变状态的能力使得很难推断程序的执行,并引入许多意想不到的副作用和错误。相比之下,声明式编程更难以编写,但避免了副作用,使得更容易理解程序的行为。
智能合约给程序员带来了很大的负担:错误会花费金钱。因此,编写不会产生意想不到的影响的智能合约至关重要。要做到这一点,你必须能够清楚地推断程序的预期行为。因此,声明式语言在智能合约中比在通用软件中扮演更重要的角色。不过,正如你将在下面看到的那样,最丰富的智能合约语言是命令式的(Solidity)。
智能合约的高级编程语言包括(按大概的年龄排序):
LLL
一种函数式(声明式)编程语言,具有类似Lisp的语法。这是以太坊智能合约的第一个高级语言,但今天很少使用。
Serpent
一种过程式(命令式)编程语言,其语法类似于Python。也可以用来编写函数式(声明式)代码,尽管它并不完全没有副作用。很少被使用。最早由Vitalik Buterin创建。
Solidity
具有类似于JavaScript,C ++或Java语法的过程式(命令式)编程语言。以太坊智能合约中最流行和最常用的语言。最初由Gavin Wood(本书的合着者)创作。
Vyper
最近开发的语言,类似于Serpent,并且具有类似Python的语法。旨在成为比Serpent更接近纯粹函数式的类Python语言,但不能取代Serpent。最早由Vitalik Buterin创建。
Bamboo
一种新开发的语言,受Erlang影响,具有明确的状态转换并且没有迭代流(循环)。旨在减少副作用并提高可审计性。非常新,很少使用。
如你所见,有很多语言可供选择。然而,在所有这些中,Solidity是迄今为止最受欢迎的,以至于成为了以太坊甚至是其他类似EVM的区块链的事实上的高级语言。我们将花大部分时间使用Solidity,但也会探索其他高级语言的一些例子,以了解其不同的哲学。
用Solidity构建智能合约
来自维基百科:
Solidity是编写智能合约的“面向合约的”编程语言。它用于在各种区块链平台上实施智能合约。它由Gavin Wood,Christian Reitwiessner,Alex Beregszaszi,Liana Husikyan,Yoichi Hirai和几位以前的以太坊核心贡献者开发,以便在区块链平台(如以太坊)上编写智能合约。
— Wikipedia entry for Solidity
Solidity由GitHub上的Solidity项目开发团队开发并维护:
https://github.com/ethereum/solidity
Solidity项目的主要“产品”是_Solidity Compiler(solc)_,它将用Solidity语言编写的程序转换为EVM字节码,并生成其他制品,如应用程序二进制接口(ABI)。Solidity编译器的每个版本都对应于并编译Solidity语言的特定版本。
要开始,我们将下载Solidity编译器的二进制可执行文件。然后我们会编写一个简单的合约。
选择一个Solidity版本
Solidity遵循一个称为_semantic versioning_(https://semver.org/)的版本模型,该模型指定版本号结构为由点分隔的三个数字:MAJOR.MINOR.PATCH。"major"用于对主要的和“向前不兼容”的更改的递增,“minor”在主要版本之间添加“向前兼容功能“时递增,“patch”表示错误修复和安全相关的更改。
目前,Solidity的版本是+0.4.21+,其中+0.4+是主要版本,21是次要版本,之后指定的任何内容都是补丁版本。Solidity的0.5版本主要版本即将推出。
正如我们在[intro]中看到的那样,你的Solidity程序可以包含一个+pragma+指令,用于指定与之兼容的Solidity的最小和最大版本,并且可用于编译你的合约。
由于Solidity正在快速发展,最好始终使用最新版本。
下载/安装
有许多方法可以用来下载和安装Solidity,无论是作为二进制发行版还是从源代码编译。你可以在Solidity文档中找到详细的说明:
https://solidity.readthedocs.io/en/latest/installing-solidity.html
在Installing solc on Ubuntu/Debian with apt package manager中,我们将使用 apt package manager 在Ubuntu/Debian操作系统上安装Solidity的最新二进制版本:
Installing solc on Ubuntu/Debian with apt package manager
$ sudo add-apt-repository ppa:ethereum/ethereum $ sudo apt update $ sudo apt install solc
一旦你安装了 solc,运行以下命令来检查版本:
$ solc --version solc, the solidity compiler commandline interface Version: 0.4.21+commit.dfe3193c.Linux.g++
根据你的操作系统和要求,还有许多其他方式可以安装Solidity,包括直接从源代码编译。有关更多信息,请参阅
https://github.com/ethereum/solidity
开发环境
要在Solidity中开发,你可以在命令行上使用任何文本编辑器和+solc+。但是,你可能会发现为开发而设计的一些文本编辑器(例如Atom)提供了附加功能,如语法突出显示和宏,这些功能使Solidity开发变得更加简单。
还有基于Web的开发环境,如Remix IDE(https://remix.ethereum.org/)和EthFiddle(https://ethfiddle.com/)。
使用可以提高生产力的工具。最后,Solidity程序只是纯文本文件。虽然花哨的编辑器和开发环境可以让事情变得更容易,但除了简单的文本编辑器(如vim(Linux / Unix),TextEdit(MacOS)甚至NotePad(Windows)),你无需任何其他东西。只需将程序源代码保存为+.sol+扩展名即可,Solidity编译器将其识别为Solidity程序。
Writing a simple Solidity program
在[intro]中,我们编写了我们的第一个Solidity程序,名为+Faucet+。当我们第一次构建+Faucet+时,我们使用Remix IDE来编译和部署合约。在本节中,我们将重新查看,改进和修饰+Faucet+。
我们的第一次尝试是这样的:
Faucet.sol : A Solidity contract implementing a faucet
// Our first contract is a faucet! contract Faucet { // Give out ether to anyone who asks function withdraw(uint withdraw_amount) public { // Limit withdrawal amount require(withdraw_amount <= 100000000000000000); // Send the amount to the address that requested it msg.sender.transfer(withdraw_amount); } // Accept any incoming amount function () public payable {} }
从 [make_it_better] 开始,我们将在第一个示例的基础上构建。
用Solidity编译器(solc)编译
现在,我们将使用命令行上的Solidity编译器直接编译我们的合约。Solidity编译器+solc+提供了多种选项,你可以通过+--help+参数来查看。
我们使用+solc+的 --bin 和 --optimize 参数来生成我们示例合约的优化二进制文件:
Compiling Faucet.sol with solc
$ solc --optimize --bin Faucet.sol ======= Faucet.sol:Faucet ======= Binary: 6060604052341561000f57600080fd5b60cf8061001d6000396000f300606060405260043610603e5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416632e1a7d4d81146040575b005b3415604a57600080fd5b603e60043567016345785d8a0000811115606357600080fd5b73ffffffffffffffffffffffffffffffffffffffff331681156108fc0282604051600060405180830381858888f19350505050151560a057600080fd5b505600a165627a7a723058203556d79355f2da19e773a9551e95f1ca7457f2b5fbbf4eacf7748ab59d2532130029
+solc+产生的结果是一个可以提交给以太坊区块链的十六进制序列化二进制文件。
以太坊合约应用程序二进制接口(ABI)
在计算机软件中,应用程序二进制接口(ABI)是两个程序模块之间的接口;通常,一个在机器代码级别,另一个在用户运行的程序级别。ABI定义了如何在*机器码*中访问数据结构和功能;不要与API混淆,API以高级的,通常是人类可读的格式将访问定义为*源代码*。因此,ABI是将数据编码到机器码,和从机器码解码数据的主要方式。
在以太坊中,ABI用于编码EVM的合约调用,并从交易中读取数据。ABI的目的是定义合约中的哪些函数可以被调用,并描述函数如何接受参数并返回数据。
合约ABI的JSON格式由一系列函数描述(参见[solidity_functions])和事件(参见[solidity_events])的数组给出。函数描述是一个JSON对象,它包含`type`,name
,inputs
,outputs
,constant`和`payable`字段。事件描述对象具有`type
,name
,`inputs`和`anonymous`的字段。
我们使用+solc+命令行Solidity编译器为我们的+Faucet.sol+示例合约生成ABI:
solc --abi Faucet.sol ======= Faucet.sol:Faucet ======= Contract JSON ABI [{"constant":false,"inputs":[{"name":"withdraw_amount","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"}]
如你所见,编译器会生成一个描述由 Faucet.sol 定义的两个函数的JSON对象。这个JSON对象可以被任何希望在部署时访问 Faucet 合约的应用程序使用。使用ABI,应用程序(如钱包或DApp浏览器)可以使用正确的参数和参数类型构造调用 Faucet 中的函数的交易。例如,钱包会知道要调用函数+withdraw+,它必须提供名为 withdraw_amount 的 uint256 参数。钱包可以提示用户提供该值,然后创建一个编码它并执行+withdraw+功能的交易。
应用程序与合约进行交互所需的全部内容是ABI以及合约的部署地址。
选择Solidity编译器和语言版本
正如我们在