# 封面

# 目录
# 前言
封面上的蜜蜂有什么含义?
蜜蜂是一种展现出高度复杂行为的物种,最终使蜂巢受益。每个蜜蜂按照一套简单的规则自由运作,并通过“跳舞”来传达重要的结果。舞蹈带有重要的信息,比如太阳的位置和从蜂巢到目标的相对地理坐标。通过解读这种舞蹈,蜜蜂可以转播这些信息或采取行动,从而实现“蜂巢思维”的意志。
虽然蜜蜂形成了一个基于种群的社会,拥有繁殖后代的蜂后,但在蜂巢中没有中央权威或领导者。数千成员的种群表现出高度智能和复杂的行为,这社会网络中的个体相互交互而形成的“涌现”特性。
自然表明,去中心化的系统可以具有弹性,并且可以产生涌现的复杂性和令人难以置信的复杂性,而不需要中央机构,层级或复杂的部分。
# 术语
这个快速术语表包含许多与以太坊相关的术语。这些术语在本书中都有使用,所以请将其加入书签以便快速参考。
账户 Account
包含地址、余额、随机数以及可选存储和代码的对象。账户可以是合约账户或外部拥有账户(EOA,externally owned account).
地址 Address
一般来说,这代表一个 EOA 或合约,它可以在区块链上接收(目标地址)或发送(源地址)交易。更具体地说,它是 ECDSA 公钥的 Keccak 散列的最右边的160位,表现为16进制的40个字符长度,在前面加上“0x”字符。
断言 Assert
在 Solidity 中,assert(false) 编译为 0xfe, 是一个无效的操作码,用尽所有剩余的燃气(Gas),并恢复所有更改。
当 assert() 语句失败时,说明发生了严重的错误或异常,你必须修复你的代码。
你应该使用 assert 来避免永远不应该发生的条件。
大端序 Big-endian
一种数值的位置表示形式,最高有效位放在最前面。对应小端序(little-endian),最低有效位在前。
比特币改进提议 BIPs
比特币改进提议,Bitcoin Improvement Proposals。比特币社区成员提交的一组提案,旨在改进比特币。例如,BIP-21是改进比特币统一资源标识符(URI)方案的建议。
区块 Block
区块是关于所包含的交易的所需信息(区块头)的集合,以及称为ommer的一组其他区块头。它被矿工添加到以太坊网络中。
区块链 Blockchain
由工作证明系统验证的一系列区块,每个区块都连接到它的前任,一直到创世区块。这与比特币协议不同,因为它没有块大小限制;它改为使用不同的燃气限制。
拜占庭分叉 Byzantium Fork
拜占庭是大都会( Metropolis )发展阶段的两大分叉之一。它包括 EIP-649:大都会难度炸弹延迟和区块奖励减少,其中冰河时代(见下文)延迟1年,而区块奖励从5个以太坊减至3个以太坊。
编译 Compiling
将高级编程语言(例如 Solidity)编写的代码转换为低级语言(例如 EVM 字节码)
共识 Consensus
大量节点,通常是网络上的大多数节点,在其本地验证的最佳区块链中都有相同的区块的情况。
不要与共识规则混淆。
共识规则 Consensus rules
完整节点为了与其他节点保持一致,遵循的区块验证规则。不要与共识混淆。
君士坦丁堡 Constantinople
大都会阶段的第二部分,2018年中期的计划。预计将包括切换到混合工作证明/权益证明共识算法,以及其他变更。
合约账户 Contract account
包含代码的账户,每当它从另一个账户(EOA 或合约)收到交易时执行。
合约创建交易 Contract creation transaction
一个特殊的交易,以“零地址”作为收件人,用于注册合约并将其记录在以太坊区块链中(请参阅“零地址”)。
去中心化自治组织 DAO
去中心化自治组织 Decentralised Autonomous Organization. 没有层级管理的公司和其他组织。也可能是指2016年4月30日发布的名为“The DAO”的合约,该合约于2016年6月遭到黑客攻击,最终在第1,192,000个区块激起了硬分叉(代号 DAO),恢复了被攻击的 DAO 合约,并导致了以太坊和以太坊经典两个竞争系统。
去中心化应用 DApp
去中心化应用 Decentralised Application. 狭义上,它至少是智能合约和 web 用户界面。更广泛地说,DApp 是一个基于开放式,分散式,点对点基础架构服务的 Web 应用程序。另外,许多 DApp 包括去中心化存储和/或消息协议和平台。
契约 Deed
ERC721提案中引入了不可替代的标记标准。与ERC20代币不同,契约证明了所有权并且不可互换,虽然它们还未在任何管辖区都被认可为合法文件,至少目前不是。
难度 Difficulty
网络范围的设置,控制产生工作量证明需要多少计算。
数字签名 Digital signature
数字签名算法是一个过程,用户可以使用私钥为文档生成称为“签名”的短字符串数据,以便具有签名,文档,和相应公钥的任何人,都可以验证(1 )该文件由该特定私钥的所有者“签名”,以及(2)该文件在签署后未被更改。
椭圆曲线数字签名算法 ECDSA
椭圆曲线数字签名算法( Elliptic Curve Digital Signature Algorithm,ECDSA )是以太坊用来确保资金只能由合法所有者使用的加密算法。
以太坊改进建议 EIP
以太坊改进建议,Ethereum Improvement Proposals,描述以太坊平台的建议标准。 EIP 是向以太坊社区提供信息的设计文档,描述新的功能,或处理过程,或环境。有关更多信息,请参见 https://github.com/ethereum/EIPs (opens new window)(另请参见下面的 ERC 定义)。
熵 Entropy
在密码学领域,表示可预测性的缺乏或随机性水平。在生成秘密信息(如主私钥)时,算法通常依赖高熵源来确保输出不可预测。
以太坊名称服务 ENS
以太坊名称服务,Ethereum Name Service. 更多信息,参见 https://github.com/ethereum/ens/ (opens new window).
外部拥有账户 EOA
外部拥有账户,Externally Owned Account. 由或为以太坊的真人用户创建的账户。
以太坊注释请求 ERC
以太坊注释请求 Ethereum Request for Comments. 一些 EIP 被标记为 ERC,表示试图定义以太坊使用的特定标准的建议。
Ethash
以太坊1.0的工作量证明算法。更多信息,参见 https://github.com/ethereum/wiki/wiki/Ethash (opens new window).
以太 Ether
以太 Ether,是以太坊生态系统中使用的本地货币,在执行智能合约时承担燃气费用。它的符合是 Ξ, 极客用的大写 Xi 字符.
Event
事件允许EVM日志工具的使用,后者可以用来在DApp的用户界面中调用JavaScript回调来监听这些事件。更多信息,参见 http://solidity.readthedocs.io/en/develop/contracts.html#events (opens new window)。
以太坊虚拟机 EVM
Ethereum Virtual Machine, 基于栈的,执行字节码的虚拟机。在以太坊中,执行模型指定了系统状态如何在给定一系列字节码指令和少量环境数据的情况下发生改变。
这是通过虚拟状态机的正式模型指定的。
EVM汇编语言 EVM Assembly Language
字节码的人类可读形式。
后备方法 Fallback function
默认的方法,当缺少数据或声明的方法名时执行。
水龙头 Faucet
一个网站,为想要在testnet上做测试的开发人员提供免费测试以太形式的奖励。
前沿 Frontier
以太坊的试验开发阶段,从2015年7月至2016年3月。
Ganache
私有以太坊区块链,你可以在上面进行测试,执行命令,在控制区块链如何运作时检查状态。
燃气 Gas
以太坊用于执行智能合约的虚拟燃料。以太坊虚拟机使用会计机制来衡量天然气的消耗量并限制计算资源的消耗。参见“图灵完备”。
燃气是执行智能合约的每条指令产生的计算单位。燃气与以太加密货币挂钩。燃气类似于蜂窝网络上的通话时间。因此,以法定货币进行交易的价格是 gas (ETH /gas)(法定货币/ETH)。
燃气限制 Gas limit
在谈论区块时,它们也有一个名为燃气限制的区域。它定义了整个区块中所有交易允许消耗的最大燃气量。
创世区块 Genesis block
区块链中的第一个块,用来初始化特定的网络和加密数字货币。
Geth
Go语言的以太坊。Go编写的最突出的以太坊协议实现之一。
硬分叉 Hard fork
硬分叉也称为硬分叉更改,是区块链中的一种永久性分歧,通常发生在非升级节点无法验证升级节点创建的遵循新共识规则的区块时。不要与分叉,软分叉,软件分叉或Git分叉混淆。
哈希值 Hash
通过哈希方法为可变大小的数据生成的固定长度的指纹。
分层确定钱包 HD wallet
使用分层确定密钥生成和传输协议的钱包(BIP32)。
分层确定钱包种子 HD wallet seed
HD钱包种子或根种子是一个可能很短的值,用作生成HD钱包的主私钥和主链码的种子。钱包种子可以用助记词表示,使人们更容易复制,备份和恢复私钥。
家园 Homestead
以太坊的第二个发展阶段,于2016年3月在1,150,000区块启动。
冰河时代 Ice Age
以太坊在200,000区块的硬分叉,提出难度指数级增长(又名难度炸弹),引发了到权益证明Proof-of-Stake的过渡。
集成开发环境 IDE (Integrated Development Environment)
集成的用户界面,结合了代码编辑器、编译器、运行时和调试器。
不可变的部署代码问题 Immutable Deployed Code Problem
一旦部署了契约(或库)的代码,它就成为不可变的。修复可能的bug并添加新特性是软件开发周期的关键。这对智能合约开发来说是一个挑战。
互换客户端地址协议 Inter exchange Client Address Protocol (ICAP)
以太坊地址编码,与国际银行帐号(IBAN)编码部分兼容,为以太坊地址提供多样的,校验和的,可互操作的编码。 ICAP地址可以编码以太坊地址或通过以太坊名称注册表注册的常用名称。他们总是以XE开始。其目的是引入一个新的IBAN国家代码:XE,X表示"extended", 加上以太坊的E,用于非管辖货币(例如XBT,XRP,XCP)。
内部交易(又称“消息”)Internal transaction (also "message")
从一个合约地址发送到另一个合约地址或EOA的交易。
Keccak256
以太坊使用的加密哈希方法。虽然在早期 Ethereum 代码中写作 SHA-3,但是由于在 2015 年 8 月 SHA-3 完成标准化时,NIST 调整了填充算法,所以 Keccak256 不同于标准的 NIST-SHA3。Ethereum 也在后续的代码中开始将 SHA-3 的写法替换成 Keccak256 。
密钥推导方法 Key Derivation Function (KDF)
也称为密码扩展算法,它被keystore格式使用,以防止对密码加密的暴力破解,字典或彩虹表攻击。它重复对密码进行哈希。
Keystore 文件
JSON 编码的文件,包含一个(随机生成的)私钥,被一个密码加密,以提供额外的安全性。
LevelDB
LevelDB是一种开源的磁盘键值存储系统。LevelDB是轻量的,单一目标的持久化库,支持许多平台。
库 Library
以太坊中的库,是特殊类型的合约,没有用于支付的方法,没有后备方法,没有数据存储。所以它不能接收或存储以太,或存储数据。库用作之前部署的代码,其他合约可以调用只读计算。
轻量级客户端 Lightweight client
轻量级客户端是一个以太坊客户端,它不存储区块链的本地副本,也不验证块和事务。它提供了钱包的功能,可以创建和广播交易。
消息 Message
内部交易,从未被序列化,只在EVM中发送。
大都会阶段 Metropolis Stage
大都会是以太坊的第三个开发阶段,在2017年10月启动。
METoken
Mastering Ethereum Token. 本书中用于演示的 ERC20 代币。
矿工 Miner
通过重复哈希计算,为新的区块寻找有效的工作量证明的网络节点。
Mist
Mist是以太坊基金会创建的第一个以太坊浏览器。它还包含一个基于浏览器的钱包,这是ERC20令牌标准的首次实施(Fabian Vogelsteller,
ERC20的作者也是Mist的主要开发人员)。Mist也是第一个引入camelCase校验码(EIP-155)的钱包。Mist运行完整节点,提供完整的DApp浏览器,支持基于Swarm的存储和ENS地址
网络 Network
将交易和区块传播到每个以太坊节点(网络参与者)的对等网络。
节点 Node
参与到对等网络的软件客户端。
随机数 Nonce
密码学中,随机数指代只可以用一次的数值。在以太坊中用到两类随机数。
- 账户随机数 - 这只是一个账户的交易计数。
- 工作量证明随机数- 用于获得工作证明的区块中的随机值(取决于当时的难度)。
Ommer
祖父节点的子节点,但它本身并不是父节点。当矿工找到一个有效的区块时,另一个矿工可能已经发布了一个竞争的区块,并添加到区块链顶部。像比特币一样,
以太坊中的孤儿区块可以被新的区块作为ommers包含,并获得部分奖励。术语 "ommer" 是对父节点的兄弟姐妹节点的性别中立的称呼,但也可以表示为“叔叔”。
Parity
以太坊客户端软件最突出的支持共同操作(多重签名)的实现之一。
权益证明 Proof-of-Stake (PoS)
权益证明是加密货币区块链协议旨在实现分布式共识的一种方法。权益证明要求用户证明一定数量的加密货币(网络中的“股份”)的所有权,以便能够参与交易验证。
工作量证明 Proof-of-Work (PoW)
一份需要大量计算才能找到的数据(证明)。在以太坊,矿工必须找到符合网络难度目标的 Ethash 算法的数字解决方案。
收据 Receipt
以太坊客户端返回的数据,表示特定交易的结果,包括交易的哈希值,其区块编号,使用的燃气量,以及在部署智能合约时的合约地址。
重入攻击 Re-entrancy Attack
当攻击者合约(Attacker contracts)调用受害者合约(Victim contracts)的方法时,可以重复这种攻击。让我们称它为victim.withdraw(),
在对该合约函数的原始调用完成之前,再次调用victim.withdraw()方法,持续递归调用它自己。
递归调用可以通过攻击者合约的后备方法实现。
攻击者必须执行的唯一技巧是在用完燃气之前中断递归调用,并避免盗用的以太被还原。
Require
在Solidity中,require(false)编译为 0xfd,它是 REVERT 操作码。REVERT指令提供了一种停止执行和恢复状态更改的方式,不消耗所有提供的燃气并且能够返回原因。
应使用require函数来确保满足有效条件,如输入或合同状态变量,或者验证调用外部合约的返回值。
在拜占庭网络升级之前,有两种实际的方式来还原交易:耗尽燃气或执行无效指令。这两个选项都消耗了所有剩余的气体。
在Byzantium网络升级之前,在黄皮书中无法找到此操作码,并且因为该操作码没有规范,所以当EVM执行到它时,会抛出一个 invalid opcode error。
还原 Revert
当需要处理与 require-sentence, require() 相同的情况,但使用更复杂的逻辑时,使用 revert()。
例如,如果你的代码有一些嵌套的 if/else 逻辑流程,你会发现使用 require-sentence, require() 而不是require()是合理的。
奖励 Reward
Ether(ETH)的数量,包含在每个新区块中的金额作为网络对找到工作证明解决方案的矿工的奖励。
递归长度前缀 Recursive Length Prefix (RLP)
RLP 是一种编码标准,由以太坊开发人员设计用来编码和序列化任意复杂度和长度的对象(数据结构)。
中本聪 Satoshi Nakamoto Satoshi Nakamoto 是设计比特币及其原始实现 Bitcoin Core 的个人或团队的名字。作为实现的一部分,他们也设计了第一个区块链。 在这个过程中,他们是第一个解决数字货币的双重支付问题的。他们的真实身份至今仍是个谜。
Vitalik Buterin
Vitalik Buterin 是俄国-加拿大的程序员和作家,以太坊和 Bitcoin 杂志的联合创始人。
Gavin Wood
Gavin Wood 是英国的程序员,以太坊的联合创始人和前 CTO。在2014年8月他提出了Solidity,用于编写智能合约的面向合约的编程语言。
密钥(私钥) Secret key (aka private key)
允许以太坊用户通过创建数字签名(参见公钥,地址,ECDSA)证明账户或合约的所有权的加密数字。
SHA
安全哈希算法 Secure Hash Algorithm,SHA 是美国国家标准与技术研究院(NIST)发布的一系列加密哈希函数。
SELFDESTRUCT 操作码
只要整个网络存在,智能合同就会存在并可执行。如果它们被编程为自毁的或使用委托调用(delegatecall)或调用代码(callcode)执行该操作,它们将从区块链中消失。
一旦执行自毁操作,存储在合同地址处的剩余Ether将被发送到另一个地址,并将存储和代码从状态中移除。
尽管这是预期的行为,但自毁合同的修剪可能或不会被以太坊客户实施。
SELFDESTRUCT 之前称作 SUICIDE, 在EIP6中, SUICIDE 重命名为 SELFDESTRUCT。
宁静 Serenity
以太坊第四个也是最后一个开发阶段。宁静还没有计划发布的日期。
Serpent
语法类似于 Python 的过程式(命令式)编程语言。也可以用来编写函数式(声明式)代码,尽管它不是完全没有副作用的。首先由 Vitalik Buterin 创建。
智能合约 Smart Contract
在以太坊的计算框架上执行的程序。
Solidity
过程式(命令式)编程语言,语法类似于 Javascript, C++ 或 Java。以太坊智能合约最流行和最常使用的语言。由 Gavin Wood(本书的联合作者)首先创造
Solidity inline assembly
内联汇编 Solidity 中包含的使用 EVM 汇编(EVM 代码的人类可读形式)的代码。内联汇编试图解决手动编写汇编时遇到的固有难题和其他问题。
Spurious Dragon
在#2,675,00块的硬分叉,来解决更多的拒绝服务攻击向量,以及另一种状态清除。还有转播攻击保护机制。
Swarm
一种去中心化(P2P)的存储网络。与 Web3 和 Whisper 共同使用来构建 DApps。
Tangerine Whistle
在 #2,463,00 块的硬分叉,改变了某些 I/O 密集操作的燃气计算方式,并从拒绝服务攻击中清除累积状态,这种攻击利用了这些操作的低燃气成本。
测试网 Testnet
一个测试网络(简称 testnet),用于模拟以太网主要网络的行为。
交易 Transaction
由原始帐户签署的提交到以太坊区块链的数据,并以特定地址为目标。交易包含元数据,例如交易的燃气限额。
Truffle
一个最常用的以太坊开发框架。包含一些 NodeJS 包,可以使用 Node Package Manager (NPM) 安装。
图灵完备 Turing Complete
在计算理论中,如果数据操纵规则(如计算机的指令集,程序设计语言或细胞自动机)可用于模拟任何图灵机,则它被称为图灵完备或计算上通用的。这个概念是以英国数学家和计算机科学家阿兰图灵命名的。
Vyper
一种高级编程语言,类似 Serpent,有 Python 式的语法,旨在接近纯函数式语言。由 Vitalik Buterin 首先创造。
钱包 Wallet
拥有你的所有密钥的软件。作为访问和控制以太坊账户并与智能合约交互的界面。请注意,密钥不需要存储在你的钱包中,并且可以从脱机存储(例如 USB 闪存驱动器或纸张)中检索以提高安全性。尽管名字为钱包,但它从不存储实际的硬币或代币。
Web3
web 的第三个版本。有 Gavin Wood 首先提出,Web3 代表了 Web 应用程序的新愿景和焦点:从集中拥有和管理的应用程序到基于去中心化协议的应用程序。
Wei
以太的最小单位, .
Whisper
一种去中心化(P2P)消息系统。与 Web3 和 Swarm 一起使用来构建 DApps。
零地址 Zero address
特殊的以太坊地址,全部是由 0
组成(即 0x0000000000000000000000000000000000000000
),被指定为创建一个智能合约所发起的交易(Transaction)的目标地址(即 to
参数的值)。
# 第一章 什么是以太坊
以太网是“世界的计算机”,这是以太坊平台的一种常见描述。这是什么意思呢?让我们首先从关注计算机科学的描述开始,然后对以太坊的功能和特性进行更实际的解读, 并将其与比特币和其他分布式账本技术(简单起见,我们将经常使用“区块链”指代)进行对比。
从计算机科学的角度来说,以太坊是一种确定性但实际上无界的状态机,它有两个基本功能,第一个是全局可访问的单例状态,第二个是对状态进行更改的虚拟机。
从更实际的角度来说,以太坊是一个开源的,全球的去中心化计算架构,执行成为 智能合约 的程序。它使用区块链来从同步和存储系统 状态,以及称为 ether 的加密货币来计量和约束执行资源成本。
以太坊平台使开发人员能够利用内置的经济学方法构建强大的去中心化应用程序。在保证持续正常运行时间的同时,还可以减少或消除审查机构,第三方接口和对手方风险。
与比特币的比较
很多之前有一些加密货币的经验人会加入以太坊,特别是比特币。以太坊与其他开放区块链共享许多通用元素:连接参与者的对等网络,用于状态同步(工作证明)的共识算法,数字货币(以太)和全局账本(区块链)。
区块链的组件
开源、公开的区块链通常包括以下组件:
- 一个连接参与者,并传播交易和包含已验证交易的区块的点对点网络,基于标准的“gossip“协议。
- 状态机中实现的一系列共识规则。
- 消息,以交易的形式表示,代表状态转移。
- 根据共识规则处理交易的状态机。
- 分布式数据库,区块链,记录所有状态转移的日志。
- 共识算法(例如,Proof-of-Work),通过强制参与者竞争并使用共识规则约束他们,来分散区块链的控制权。
- 上述内容的一个或多个开源软件实现。
所有或大部分这些组件通常组合在一个软件客户端中。例如,在比特币中,参考实现由 Bitcoin Core 开源项目开发,并作为 bitcoind
客户端实现。在以太坊中,没有参考实现,而是 参考规范,是在 yellowpaper 中对系统的数学描述。有许多客户端根据参考规范建造。
过去,我们使用术语“区块链”来表示上述所有组件,作为包含上述所有特性的技术组合的简称。然而,今天,区块链这个词已经被营销商 和奸商所淡化,他们期待炒作他们的项目并为其创业公司实现不切实际的估值。它自己实际上是毫无意义的。我们需要限定词来帮助我们理解这些区块链的特征, 例如 开源,公开,全球,分散,中立和抗审查等,以确定这些组件给予“区块链”系统的重要涌现特征。
并不是所有的区块链都是相同的。当你被告知某样东西是区块链时,你还没有得到答案,你需要问很多问题来澄清“区块链”是什么意思。 首先询问上面组件的描述,然后询问这个“区块链”是否显示了 开源、公开 等特性。
以太坊的开发
以太坊的目标和构建在很多方面都和之前的开源区块链有所不同,包括比特币。 以太坊的目的主要不是数字货币支付网络。但数字货币ether对于以太坊的运作来说既是不可或缺的也是必要的,以太也被视为一种实用货币来支付以太坊平台的使用。
与具有非常有限的脚本语言的比特币不同,以太坊被设计成一个通用可编程区块链,运行一个虚拟机,能够执行任意和无限复杂的代码。 比特币的脚本语言故意被限制为简单的真/假消费条件判断,以太坊的语言是图灵完备的,这意味着它相当于一台通用计算机, 可以运行理论图灵机可以运行的任何计算。
以太坊的诞生
所有伟大的创新都解决了真正的问题,以太坊也不例外。当人们认识到比特币模型的力量,并试图超越加密货币应用,转向其他项目时,人们构思出了以太坊。 但开发人员面临着一个难题:要么在比特币之上构建,要么启动一个新的区块链。以比特币为基础意味着处于网络的有意约束之中,并试图找到解决方法。 数据存储的有限类型和大小似乎限制了可以在其上作为第二层解决方案运行的应用程序的类型。程序员需要构建仅使用有限的变量,交易类型和数据集的系统。 对于需要更多自由度和更大灵活性的项目,启动新的区块链是唯一的选择。但开始一个新的区块链意味着要构建所有的基础设施元素,测试等。
2013年底,年轻程序员和比特币爱好者Vitalik Buterin开始考虑进一步扩展比特币和Mastercoin(一种扩展比特币,提供基本智能合约的叠加协议) 的功能。 2013年10月,Vitalik向Mastercoin团队提出了一个更通用的方法,该方案允许用灵活且可编写脚本(但不是图灵完备的)的合约取代Mastercoin的专业合约语言。虽然Mastercoin团队印象深刻,但这一提议太过激进,无法适应他们的发展路线图。
2013年12月,Vitalik开始分享一份白皮书,描述了以太坊背后的想法:一个图灵完备的可编程和通用区块链。几十个人看到了这个早期的草案,并向Vitalik提供了反馈,帮助他逐渐提出提案。
本书的两位作者都收到了白皮书的初稿,并对其进行了评论。Andreas M. Antonopoulos 对这个想法很感兴趣,并向Vitalik询问了很多关于使用单独的区 块链实施智能合约执行的共识规则以及图灵完备语言的影响等问题。Andreas非常关注以太坊的进展,但他正在写作“Mastering Bitcoin”一书的早期阶段,直到很久以后才直接参与以太坊。然而,Gavin Wood博士是第一批接触Vitalik并提供帮助提供、 C ++编程技能的人员之一。Gavin成为了以太坊的联合创始人,联合设计师和CTO。
正如Vitalik在他的 https://vitalik.ca/general/2017/09/14/prehistory.html (opens new window) 中所述:
当时的以太坊协议完全是我自己的创作。然而,从这里开始,新的参与者开始加入。迄今为止协议方面最突出的是Gavin Wood。
...
将以太坊视为构建可编程金钱的平台而带来的微妙变化也可以归功于Gavin,基于区块链的合约可以保存数字资产并根据预设规则将其转移到通用计算平台。 这起始于着重点和术语的细微变化,随着对“Web 3”体系的日益重视,这种影响变得更加强烈,这种体系将Ethereum看作是一套去中心化技术的组成部分,另外两个是Whisper和Swarm。
从2013年12月开始,Vitalik和Gavin完善并发展了这个想法,共同构建了形成以太坊的协议层。
以太坊的创始人们正在考虑一个并非针对特定目的的区块链,而是通过成为可编程的来支持各种各样的应用。这个想法是,通过使用像以太坊这样的通用区块链, 开发人员可以编写他们的特定应用程序,而不必开发对等网络,区块链,共识算法等底层机制。以太坊平台旨在抽象这些详细信息并为去中心化区块链应用程序提供确定性和安全的编程环境。
就像Satoshi一样,Vitalik和Gavin不仅仅发明了一种新技术,他们以新颖的方式将新发明与现有技术结合起来,并提供了原型代码以向世界证明他们的想法。
创始人多年来一直致力于构建和完善愿景。2015年7月30日,第一个以太坊地块被开采。世界计算机开始为世界服务......
Vitalik Buterin的文章“以太坊史前史”于2017年9月出版,提供了以太坊最早时刻的迷人第一人称视角。
你可以在 https://vitalik.ca/general/2017/09/14/prehistory.html (opens new window) 阅读。
以太坊开发的四个阶段
以太坊的诞生是第一阶段的启动,名为“前沿(Frontier)”。以太坊的发展计划分四个阶段进行,每个新阶段都会发生重大变化。每个阶段都可能包含子版本,称为“硬分叉”,它们以不向后兼容的方式改变功能。
四个主要的发展阶段代号为前沿(Frontier),家园(Homestead),大都会(Metropolis)和宁静(Serenity)。中间的硬分叉代号为“冰河时代(Ice Age)”,“DAO”, “蜜桔前哨(Tangerine Whistle)”,“假龙(Spurious Dragon)”,“拜占庭(Byzantium)”和“君士坦丁堡(Constantinople)”。它们在下面列出,以及硬分叉发生的块号:
之前的过渡
Block #0:: "Frontier" - 以太坊的初始阶段, 从2015年7月30日持续到2016年3月。
Block #200,000:: "Ice Age" - 引入指数级难度增长的一个难题,激励了到权益证明的过渡。
Block #1,150,000:: "Homestead" - 以太坊的第二阶段,2016年3月启动。
Block #1,192,000:: "DAO" - 恢复被破坏的DAO合约的硬分叉,导致以太坊和以太坊经典分成两个竞争系统。
Block #2,463,000:: "Tangerine Whistle" - 改变某些IO密集操作的燃气计算方法和清除拒绝服务攻击(利用这些操作的低燃气成本)累积状态的硬分叉。
Block #2,675,000:: "Spurious Dragon" - 解决更多拒绝服务攻击向量和另一种状态清除的硬分叉,还包括转播攻击保护机制。
当前状态
我们目前位于Metropolis阶段,该阶段计划为两个次级版本的硬分叉 (参见 hardfork) ,代号 Byzantium 我 Constantinople。拜占庭于2017年10月生效,君士坦丁堡预计将在2018年中期。
Block #4,370,000:: “大都会拜占庭” - 大都会是以太坊的第三阶段,正是撰写本书的时间,于2017年10月启动。拜占庭是Metropolis的两个硬分叉中的第一个。
未来的计划
在大都会拜占庭硬分叉之后,大都会还有一个硬分叉计划。大都会之后是以太坊部署的最后阶段,代号为Serenity。
Constantinople:: - 大都会阶段的第二部分,计划在2018年中期。预计将包括切换到混合的工作证明/权益证明共识算法,以及其他变更。
Serenity:: 以太坊的第四个也是最后一个阶段。宁静尚未有计划的发布日期。
以太坊:通用的区块链
原始区块链(比特币的区块链)追踪比特币单位的状态及其所有权。你可以将比特币视为分布式共识 状态机,其中交易引起全局的状态转移 ,从而更改比特币的所有权。 状态转移受共识规则的制约,允许所有参与者(最终)在开采数个区块后在系统的共同(共识)状态上汇合。
以太坊也是一个分布式状态机。但是,不仅仅追踪货币所有权的状态,以太坊追踪通用数据存储的状态转换。通常我们指的是任何可以表示为 键值对 key-value tuple的数据。 键值数据存储简单地存储任何通过某个键引用的值。例如,存储由“Book Title”键引用的值“Mastering Ethereum”。在某些方面,这与通用计算机使用的 Random访问存储器(RAM) 的数据存储模型具有相同的用途。以太坊有 memory 存储代码和数据,它使用以太坊区块链来跟踪这些内存随着时间的变化。就像通用的存储程序的计算机一样, 以太坊可以将代码加载到其状态机中并运行该代码,将结果状态更改存储在其区块链中。与通用计算机的两个重要差异在于,以太坊状态的变化受共识规则的支配,并且状态通过共享账本全球分布。 以太坊回答了这样一个问题:“跟踪任何状态并对状态机进行编程,以创建一个在共识之下运行的全球计算机会怎样?”。
以太坊的组件
在Ethereum中,blockchain_components 中描述的区块链系统组件包括:
P2P Network 以太坊在 以太坊主网 上运行,可以通过TCP端口30303访问,运行称作 ÐΞVp2p 的协议。
Consensus rules 以太坊的共识规则,在参考规范,即 yellowpaper 中定义。
Transactions Ethereum交易(参见transactions)是网络消息,包括发送者,接收者,值和数据负载等。
State Machine 以太坊的状态转移由 Ethereum虚拟机(EVM) 处理,这是一个执行 bytecode(机器语言指令)的基于栈的虚拟机。称为“智能合约”的EVM程序以高级语言(如Solidity)编写,并编译为字节码以便在EVM上执行。
Blockchain 以太坊的区块链作为 database(通常是Google的LevelDB)存储在每个节点上,该区块链在称作 梅克尔帕特里夏树 Merkle Patricia Tree 的序列化哈希数据结构中包含交易和系统状态。
Consensus Algorithm 以太坊目前使用名为Ethash的工作量证明算法,但有计划在不久的将来将过渡到称为Casper的权益证明(Proof-of-Stake)系统。
Clients 以太坊有几个可互操作的客户端软件实现,其中最突出的是 Go-Ethereum(Geth)和Parity。
其他参考文献
以太坊黄皮书: https://ethereum.github.io/yellowpaper/paper.pdf (opens new window)
褐皮书”:为更广泛的读者以不太正式的语言重写了“黄皮书”: https://github.com/chronaeon/beigepaper (opens new window)
ÐΞVp2p 网络协议: https://github.com/ethereum/wiki/wiki/%C3%90%CE%9EVp2p-Wire-Protocol (opens new window)
以太坊状态机 —— 一个“Awesome”资源列表 https://github.com/ethereum/wiki/wiki/Ethereum-Virtual-Machine-(EVM)-Awesome-List (opens new window)
LevelDB 数据库 (最经常用于存储区块链本地副本): http://leveldb.org (opens new window) Merkle Patricia Trees: https://github.com/ethereum/wiki/wiki/Patricia-Tree (opens new window)
Ethash 工作量证明共识算法: https://github.com/ethereum/wiki/wiki/Ethash (opens new window)
Casper 权益证明 v1 实现指南: https://github.com/ethereum/research/wiki/Casper-Version-1-Implementation-Guide (opens new window)
Go-Ethereum (Geth) 客户端: https://geth.ethereum.org/ (opens new window)
Parity 以太坊客户端: https://parity.io/ (opens new window)
以太坊和图灵完整性
只要你开始阅读关于以太坊的信息,你将立即听到“图灵完成”一词。他们说,与比特币不同,以太坊是“图灵完成”。这到底是什么意思呢?
术语“图灵完全”是以英国数学家阿兰图灵(Alan Turing)的名字命名的,他被认为是计算机科学之父。1936年,他创建了一个计算机的数学模型,该计算机由一个状态机构成, 该状态机通过读写顺序存储器(类似于无限长度的磁带)来操纵符号。通过这个构造,Alan Turing继续提供了一个来回答(否定的)关于 通用可计算性(是否可以解决所有问题) 问题的数学基础。他证明了存在一些不可计算的问题。具体来说,他证明 停机问题 Halting Problem(试图评估程序是否最终会停止运行)是不可解决的。
Alan Turing进一步将系统定义为Turing Complete,如果它可以用来模拟任何图灵机。这样的系统被称为 通用图灵机 Universal Turing Machine(UTM)。
以太坊在一个名为以太坊虚拟机的状态机中执行存储程序,在内存中读写数据的能力,使其成为一个图灵完整系统,因此是一台通用图灵机。对于有限的存储,以太坊可以计算任何图灵机可以计算的算法。
以太坊的突破性创新是将存储程序计算机的通用计算架构与去中心化区块链相结合,从而创建分布式单状态(单例)世界计算机。以太坊程序“到处”运行,但却产生了共识规则所保证的共同(共识)状态。
图灵完备是一个“特性”
听说以太坊是图灵完备的,你可能会得出这样的结论:这是一个图灵不完备系统中缺乏的功能。相反,情况恰恰相反。需要努力来限制一个系统,使它不是 Turing Complete 的。 即使是最简单的状态机也会出现图灵完备性。事实上,已知最简单的Turing Complete状态机(Rogozhin,1996)具有4个状态并使用6个符号,状态定义只有22个指令长。
图灵完备不仅可以最简单的系统中实现,而且有意设计为受限制的图灵不完备的系统通常被认为是“意外图灵完备的”。图灵不完备的约束系统更难设计,必须仔细维护,以保持图灵不完备。
关于“意外图灵完备的”的有趣的参考资料可以在这里找到: http://beza1e1.tuxen.de/articles/accidentally_turing_complete.html (opens new window)
以太坊是图灵完备的事实意味着任何复杂的程序都可以在以太坊中计算。但是这种灵活性带来了一些棘手的安全和资源管理问题。
图灵完备的含义
图灵证明,你无法通过在计算机上模拟程序来预测程序是否会终止。简而言之,我们无法预测程序的运行路径。图灵完备系统可以在“无限循环”中运行,这是一个用于描述不终止程序的术语(过分简化地说)。 创建一个运行永不结束的循环的程序是微不足道的。但由于起始条件和代码之间存在复杂的相互作用,无意识的无限循环可能会在没有警告的情况下产生。在以太坊中,这提出了一个挑战:每个参与节点(客户端) 必须验证每个交易,运行它所调用的任何智能合约。但正如图灵证明的那样,以太坊在没有实际运行(可能永远运行)时,无法预测智能合约是否会终止,或者运行多久。可以意外,或有意地,创建智能合约, 使其在节点尝试验证它时永久运行,实际上是拒绝服务攻击。当然,在需要毫秒验证的程序和永远运行的程序之间,存在无限范围的令人讨厌的资源浪费,内存膨胀,CPU过热程序,这些程序只会浪费资源。在世界计算机中, 滥用资源的程序会滥用世界资源。如果以太坊无法预测资源使用情况,以太坊如何限制智能合约使用的资源?
为了应对这一挑战,以太坊引入了称为 燃气 gas的计量机制。随着EVM执行智能合约,它会仔细考虑每条指令(计算,数据访问等)。每条指令都有一个以燃气为单位的预定成本。 当交易触发智能合约的执行时,它必须包含一定量的燃气,用以设定运行智能合约可消耗的计算上限。如果计算所消耗的燃气量超过交易中可用的天然气量,则EVM将终止执行。Gas是以太坊用于允许图灵完备计算的机制,同时限制任何程序可以使用的资源。
2015年,攻击者利用了一个成本远低于应有成本的EVM指令。这允许攻击者创建使用大量内存的交易,并花几分钟时间进行验证。为了解决这一攻击, 以太坊必须在不向前兼容(硬分叉)的更改中改变特定指令的燃气核算公式。但是,即使有这种变化,以太坊客户端也不得不跳过验证这些交易或浪费数周的时间来验证这些交易。
从通用区块链到去中心化应用 (DApps)
以太坊作为一种可用于各种用途的通用区块链的方式开始。但很快,以太坊的愿景扩展为编程 去中心化应用(DApps) 的平台。DApps代表比“智能合约”更广阔的视角。 DApp至少是一个智能合约和一个web用户界面。更广泛地说,DApp是一个基于开放的,去中心化的,点对点基础架构服务的Web应用程序。
DApp至少由以下部分组成:
- 区块链上的智能合约
- 一个Web前端用户界面
另外,许多DApp还包括其他去中心化组件,例如:
- 去中心化(P2P)存储协议和平台。
- 去中心化(P2P)消息传递协议和平台。
TIP
你可能会看到DApps拼写为 ÐApps. Ð 字符是拉丁字符,称为“ETH”,暗指以太坊。"ETH", 要显示此字符,请在HTML中使用十进制实体 #208
,并使用Unicode字符 0xCE
(UTF-8)或 0x00D0
(UTF-16)。
万维网的进化
2004年,“Web 2.0”一词引人注目,描述了网络向用户生成内容,响应接口和交互性的演变。Web 2.0不是技术规范,而是描述Web应用程序新焦点的术语。
DApps的概念旨在将万维网引入其下一个自然演进,将去中心化对等协议引入Web应用程序的每个方面。用于描述这种演变的术语是 Web3,意思是网络的第三个“版本”。由Gavin Wood首先提出, web3代表了Web应用程序的新愿景和焦点:从集中拥有和管理的应用程序到基于去中心化协议的应用程序。
在后面的章节中,我们将探索Ethereum web3js
JavaScript库,它将你的浏览器中运行的JavaScript应用程序与以太坊区块链连接起来。web3.js
库还包含一个名为 Swarm
的P2P存储网络接口和一个称为 Whisper 的P2P消息传递服务。通过在你的Web浏览器中运行的JavaScript库中包含这三个组件,开发人员可以使用完整的应用程序开发套件来构建web3 DApps:

以太坊的开发文化
到目前为止,我们已经谈到了以太坊的目标和技术与其他区块链之前的区别,比如比特币。以太坊也有非常不同的开发文化。
在比特币中,开发以保守原则为指导:所有变化都经过仔细研究,以确保现有系统都不会中断。大部分情况下,只有在向后兼容时才会执行更改。允许现有客户“选择加入”,但如果他们决定不升级,将继续运作。
相比之下,在以太坊中,开发文化的重点是速度和创新。这个咒语是“快速行动,解决事情”。如果需要进行更改,即使这意味着使之前的假设失效,破坏兼容性或强制客户端进行更新,也会执行更改。 以太坊的开发文化的特点是快速创新,快速进化和愿意参与实验。
这对开发者来说意味着什么,就是你必须保持灵活性,随着一些潜在的假设变化,准备重建你的基础设施。不要以为任何东西都是静态的或永久的。以太坊开发人员面临的一个重大挑战是将代码部署到不可变账本与仍在快速发展的开发平台之间的内在矛盾。 你不能简单地“升级”你的智能合约。你必须准备部署新的,迁移用户,应用程序和资金,并重新开始。
具有讽刺意味的是,这也意味着构建具有更多自主权和更少集中控制的系统的目标是无法实现的。在接下来的几年中,自治和分权要求平台中的稳定性要比以太坊可能获得的稳定性要高一点。为了“发展”平台,你必须准备好取消并重启你的智能合约,这意味着你必须保留一定程度的控制权。
但是,在积极的一面,以太坊正在快速发展。“自行车脱落”的机会很小 - 这个表达意味着争论一些小细节,比如如何在大楼后面建造自行车棚。如果你开始骑脚踏车,你可能会突然发现其他的开发团队改变了计划,并且抛弃了自行车,转而使用自动气垫船。 在以太坊有很少的神圣原则,最终标准或固定接口。
最终,以太坊核心协议的开发速度将会放慢,其接口将会变得固定。但与此同时,创新是推动原则。你最好跟上,因为没有人会为你放慢速度。
为什么学习以太坊?
区块链具有非常陡峭的学习曲线,因为它们将多个学科合并到一个领域:编程,信息安全,密码学,经济学,分布式系统,对等网络等。以太坊使得这一学习曲线不再陡峭,因此你可以很快就开始了。但就在一个看似简单的环境表面之下,还有更多。 当你学习并开始更深入的观察时,总会有另一层复杂性和奇迹。
以太坊是学习区块链的绝佳平台,它构建了一个庞大的开发者社区,比任何其他区块链平台都快。相比其他区块链,以太坊是开发者为开发者开发的开发者的区块链。熟悉JavaScript应用程序的开发人员可以进入以太坊并开始快速生成工作代码。 在以太坊的头几年,通常看到T恤衫宣布你可以用五行代码创建一个代币。当然,这是一把双刃剑。编写代码很容易,但编写good代码和secure代码非常困难。
本书将教你什么?
这本书深入以太坊的每一个组成部分。你将从一个简单的交易开始,分析它的工作原理,建立一个简单的合约,使其更好,并跟踪它在以太坊系统中的路径。
你将了解以太坊的工作方式,以及为什么这样设计。你将能够理解每个组成部分的工作方式,它们如何组合在一起以及为什么。
# 第二章 以太坊基础
控制和责任
像以太坊这样的开放区块链是安全的,因为它们是_去中心化的_。这意味着以太坊的每个用户都应该控制自己的密钥,这些密钥可以控制对资 金和合约的访问。一些用户选择通过使用第三方保管人(比如交易所钱包)放弃对密钥的控制权。在本书中,我们将教你如何控制和管理你自 己的密钥。
这种控制带来了很大的责任。如果你丢失了你的钥匙,你将无法获得资金和合约。没有人可以帮助你重新获得访问权 - 你的资金将永远锁定。 以下是一些帮助你管理这一责任的提示:
提示你选择密码时:强化它,备份并不共享。如果你没有密码管理器,请将其写下并存放在锁定的抽屉或保险柜中。只要你拥有帐户的 “keystore”文件,就可以使用此密码。
当系统提示你备份密钥或助记词时,请使用笔和纸进行物理备份。不要把这个任务放在“以后”,你会忘记。这些在你丢失了系统中保存 的所有数据时使用。
不要在数字文档,数字照片,屏幕截图,在线驱动器,加密的PDF等中存储密钥材料(加密或不加密),不要临时凑合的安全性。使用密 码管理器或笔和纸。
在传输任何大量数据之前,先做一个小的测试交易(例如,1美元)。一旦你收到测试交易,请尝试从该钱包中发送。如果你忘记密码或 因任何其他原因无法发送资金,最好是一个小小的损失。
请勿将金钱汇入本书所示的任何地址。私人密钥被列在书中,有人会立即拿走这笔钱。
以太网货币单位
以太坊的货币单位称为 以太 ether,也被称为ETH或符号 Ξ (来自看起来像程式化的大写字母E的希腊字母“Xi”)或(不太常 见的)♦,例如,1个以太,或1个ETH,或 Ξ1,或 ♦1
TIP
Ξ 使用Unicode字符926,♦使用9830。
以太被细分为更小的单位,直到可能的最小单位,这被命名为_wei_。一个 ether 是 或1,000,000,000,000,000,000 个 wei。你可能会听到人们也提到货币“以太坊”,但这是一个常见的初学者的错误。以太坊是制度,以太是货币。
以太的值总是在以太坊内部表示为_wei_,无符号整数值。当你处理1个以太网时,交易将编码10000000000000000000 wei作为值。
以太的各种单位既有使用国际单位系统(SI)的科学名称,也有口语化的名字,向计算机和密码学的许多伟大思想致敬。
表 ether_denominations 显示了各种单位,它们的俗名(通用)名称和他们的SI名称。为了与价值的内部 表示保持一致,该表格显示了所有面值的wei(第一行),在第七行中 ether 显示为 :
Value (in wei) | Exponent | Common Name | SI Name |
1 | 1 | wei | wei |
1,000 | 103 | babbage | kilowei or femtoether |
1,000,000 | 106 | lovelace | megawei or picoether |
1,000,000,000 | 109 | shannon | gigawei or nanoether |
1,000,000,000,000 | 1012 | szabo | microether or micro |
1,000,000,000,000,000 | 1015 | finney | milliether or milli |
1,000,000,000,000,000,000 | 1018 | ether | ether |
1,000,000,000,000,000,000,000 | 1021 | grand | kiloether |
1,000,000,000,000,000,000,000,000 | 1024 | megaether |
选择一个以太坊钱包
以太坊钱包是你通往以太坊系统的门户。它拥有你的密钥,并且可以代表你创建和广播交易。选择一个以太坊钱包可能很困 难,因为有很多不同功能和设计选择。有些更适合初学者,有些更适合专家。即使你现在选择一个你喜欢的,你可能会决定 稍后切换到另一个钱包。以太坊本身在不断变化,“最好”的钱包往往是适应它们的。
别担心!如果你选择一个钱包而不喜欢它的工作方式,那么你可以很容易地更换钱包。你只需进行一项交易,即将资金从旧 钱包发送到新钱包,或者通过导出和导入私钥来移动密钥。
首先,我们将选择三种不同类型的钱包作为整本书的示例:移动钱包,桌面钱包和基于网络的钱包。我们选择了这三款钱 包,因为它们代表了广泛的复杂性和功能。然而,选择这些钱包并不是对其质量或安全性的认可。他们只是示范和测试。
起始钱包:
MetaMask MetaMask是一款浏览器扩展钱包,可在你的浏览器(Chrome,Firefox,Opera或Brave Browser)中运行。它易于使用 且便于测试,因为它可以连接到各种以太坊节点和测试区块链(请参阅“testnets”)。
Jaxx Jaxx是一款多平台和多币种钱包,可在各种操作系统上运行,包括Android,iOS,Windows,Mac和Linux。对于新用户 来说,它通常是一个不错的选择,因为它的设计简单易用。
MyEtherWallet (MEW) MyEtherWallet是一款基于网络的钱包,可在任何浏览器中运行。它具有多个复杂的功能,我们将在许多示例中探讨这 些功能。
Emerald Wallet Emerald钱包设计用于以太坊经典区块链,但与其他以太坊区块链兼容。它是一款开源桌面应用程序,适用于Windows,Mac和Linux。 Emerald钱包可以运行一个完整的节点或连接到一个公共的远程节点,工作在“轻量”模式下。它还有一个配套工具来在命令行中执行所有操作。
我们将首先在桌面上安装MetaMask
# 安装 MetaMask
打开Google Chrome浏览器并导航至:
https://chrome.google.com/webstore/category/extensions (opens new window)
搜索“MetaMask”并点击狐狸的标志。你应该看到这样的扩展的详细信息页面:

Figure 1. The detail page of the MetaMask Chrome Extension
验证你是否下载真正的MetaMask扩展非常重要,因为有时候人们可以将恶意扩展通过Google的过滤器。
- 在地址栏中显示ID
nkbihfbeogaeaoehlefnkodbefgpgknn
- 由https://metamask.io提供
- 有超过800个评论
- 拥有超过1,000,000名用户
确认你正在查看正确的扩展程序后,请点击“添加到Chrome”进行安装。
第一次使用MetaMask
一旦安装了MetaMask,你应该在浏览器的工具栏中看到一个新图标(狐狸头)。点击它开始。它将要求你接受条款和条件,然后通过输入密码来创建新的 以太坊钱包:

Figure 2. The password page of the MetaMask Chrome Extension
TIP
密码控制对MetaMask的访问,任何有权访问你的浏览器的人无法使用它。
一旦你设置了密码,MetaMask将为你生成一个钱包并向你显示一个_助记词备份_,由12个英文单词组成。如果MetaMask或 你的计算机出现问题,可以在任何兼容的钱包中使用这些词来恢复对资金的访问。你不需要通过密码进行恢复。这12个字就足够了。

Figure 3. The mnemonic backup of your wallet, created by MetaMask
TIP
在纸上备份助记符(12个字),两次。将两份纸张备份存放在两个单独的安全位置,例如防火安全柜,锁定的抽屉或保险箱。 将纸质备份视为你在Ethereum钱包中存储的相同价值的现金。任何能够访问这些文字的人都可以访问并窃取你的资金。
一旦确认你已安全存储助记符,MetaMask将显示你的以太坊帐户详细信息:

你的帐户页面会显示你帐户的名称(默认情况下为“Account 1”),以太坊地址(示例中为0x9E713 ...)以及彩色图标,以帮 助你将此帐户与其他帐户区分开来。在帐户页面的顶部,你可以看到你当前正在使用哪个以太坊网络(示例中的“主网络”)。
恭喜!你已经建立了你的第一个以太坊钱包!
切换网络
正如你在MetaMask帐户页面上所看到的,你可以在多个以太坊网络中进行选择。默认情况下,MetaMask将尝试连接到“主 网络”。其他选择是公共测试网,你选择的任何以太坊节点或在你自己的计算机上运行私有区块链的节点(本地主机):
Main Ethereum Network
主要的,公开的以太坊区块链。真正的ETH,真正的价值,真正的后果。
Ropsten Test Network
以太坊公开测试区块链和网络,使用工作证明共识(挖矿)。在这个网络上的ETH没有价值。Ropsten的问题在于攻击者铸造了数以万计的区块,产生巨大的重组并将燃气极限推到9B。当时需要一个新的公共测试网,但之后(2017年3月25日)Ropsten也复活了!
Kovan Test Network
以太坊公开测试区块链和网络,使用“Aura”协议进行权威证明(Proof-of-Authority)共识(联合签名)。在这个网络上的ETH没有价值。该测试网络仅由“Parity”支持。其他以太坊客户使用稍后提出的"Clique"协议作为权威证明。
Rinkeby Test Network
以太坊公开测试区块链和网络,使用“Clique”协议进行权威证明共识(联合签名)。在这个网络上的ETH没有价值。
Localhost 8545
连接到与浏览器在同一台计算机上运行的节点。该节点可以是任何公共区块链(主要或测试网络)或私人测试网络的一部分(参见ganache)。
Custom RPC
允许你将MetaMask连接到任何具有geth兼容的远程过程调用(RPC)接口的节点。该节点可以是任何公共或私有区块链的一部分。
有关各种以太坊测试网以及如何在它们之间进行选择的更多信息,请参见 testnets。
TIP
你的MetaMask钱包在连接的所有网络上使用相同的私钥和以太坊地址。但是,每个以太坊网络上的以太坊地址余额将有所不同。例如, 你的密钥可以控制Ropsten上的以太和合约,但不能控制主网上的。
获得一些测试以太
我们的首要任务是给我们的钱包充值。我们不会在主网上这样做,因为真正的以太网需要花费金钱,处理它需要更多的经验。现在, 我们将使用一些testnet ether加载我们的钱包。
将MetaMask切换到_Ropsten测试网络_。然后点击“Buy”,然后点击“Ropsten Test Faucet”。MetaMask将打开一个新的网页:

Figure 5. MetaMask Ropsten Test Faucet 你可能会注意到该网页已经包含你的MetaMask钱包的以太坊地址。MetaMask集成了支持以太坊的网页( 参见 [dapps])与你的MetaMask 钱包整合在一起。MetaMask可以在网页上“查看”以太坊地址,例如,你可以向显示以太坊地址的网上商店发送付款。如果网页请求, MetaMask也可以使用自己的钱包地址填入网页,作为收件人地址。在此页面中,faucet应用程序要求MetaMask提供一个钱包地址 以发送测试以太网。
按绿色"request 1 ether from faucet"按钮。你会看到一个交易ID出现在页面的下方。faucet应用程序创建了一个交易 - 付款 给你。交易ID如下所示:
0x7c7ad5aaea6474adccf6f5c5d6abed11b70a350fbc6f9590109e099568090c57
几秒钟后,新交易将由Ropsten矿工开采,你的MetaMask钱包将显示1 ETH的余额。点击交易ID,你的浏览器会将你带到一个
block explorer,该网站允许你查看和浏览区块,地址和交易。MetaMask使用 etherscan.io
区块浏览器,这是受欢迎
的以太坊区块浏览器之一。包含Ropsten Test Faucet支付的交易显示在 ropsten_block_explorer 中。

Figure 6. Etherscan Ropsten Block Explorer
交易记录在Ropsten区块链中,任何人都可以随时查看,只需搜索交易ID或访问链接即可:
尝试访问该链接,或将交易哈希值输入到 ropsten.etherscan.io
网站中,亲自查看。
使用MetaMask发送ether
一旦我们从Ropsten Test Faucet接收到我们的第一个测试ether,我们将试着发送一些ether回到faucet。正如你在Ropsten Test Faucet页面上看到的那样,你可以选择“donate”1个ETH。这个选项是可用的,所以一旦你完成了测试,你可以返回剩余的测试ether,以便其他人可以使用它。尽管测试ether没有价值,但有些人囤积测试ether,使其他人难以使用测试网络。囤积测试ether令人不悦!
幸运的是,我们不是测试ether的囤积者,我们希望练习发送ether。
点击橙色的“1 ether”按钮来告诉MetaMask创建支付Faucet 1 ether的交易。MetaMask将准备一个交易并弹出一个窗口,并显示确认信息:

Figure 7. Sending 1 ether to the faucet
哎!你可能注意到你无法完成交易。MetaMask表示“交易余额不足”。乍一看这可能会让人困惑:我们有1个ETH,我们想要 发送1个ETH,为什么MetaMask说我们没有足够的资金?
答案是因为_gas_的成本。以太坊交易需要支付矿工收取的费用,以验证交易。以太坊的费用以_gas_虚拟货币收取。作为交 易的一部分,你使用ether支付gas。
TIP
测试网络也需要费用。如果没有费用,测试网络的行为将与主网络不同,从而使其成为不适当的测试平台。费用还可以保护测试 网络免受拒绝服务攻击和构造不良的合约(如无限循环),就像保护主网络一样。
当你发送交易时,Metamask以3 GWEI(即3 gigawei)计算最近成功交易的平均gas价格。Wei是以太货币的最小的细分,正如我们在 ether_units 中所讨论的那样。发送基本交易的gas成本为21000个gas单位。因此,你花费的ETH的最大数量为 。 请注意,平均gas价格可能波动,因为它们主要由矿工决定。我们将在后面的章节中看到如何增加/减少gas限制,以确保你的交易在需要时优先处理。
这表明:1 ETH交易的成本是1.000063 ETH。MetaMask在显示总数时会将此近似到1 ETH,但你需要的实际金额为1.000063 ETH,并且你只有1个ETH。 点击“Reject”取消此交易。
让我们再来测试一下吧!再次点击绿色的“request 1 ether from the faucet”按钮,等待几秒钟。别担心,faucet应该有足够的ether,如果你要的 话,会给你更多的东西。
一旦你有2 ETH的余额,你可以再试一次。这次,当你点击橙色的“1 ether”捐赠按钮时,你有足够的余额来完成交易。MetaMask弹出付款窗口时 点击“Submit”。所有这一切之后,你应该看到0.999937 ETH的余额,因为你使用0.000063 ETH的gas发送了1个ETH到faucet。
探索地址的交易历史
到目前为止,你已经成为使用MetaMask发送和接收测试ether的专家。你的钱包已收到至少两次付款并至少发送了一次。让我们看看所有这些交易,
使用 ropsten.etherscan.io
区块浏览器。你可以复制你的钱包地址并将其粘贴到浏览器的搜索框中,或者你可以让MetaMask为你打开该页面。
在MetaMask中你的帐户图标旁边,你会看到一个显示三个点的按钮。点击它显示与帐户相关的选项菜单:

Figure 8. MetaMask Account Context Menu
选择 "View Account on Etherscan",在浏览器中打开一个网页,显示你账户的交易记录:

Figure 9. Address Transaction History on Etherscan
在这里你可以看到你的以太坊地址的整个交易历史。它显示了Ropsten区块链上记录的所有交易,其中你的地址是交易的发件人或收件人。点击其中几项交易即可查看更多详细信息。
你可以浏览任何地址的交易历史记录。查看你是否可以浏览Ropsten Test Faucet地址的交易历史记录(提示:它是在你的地址中最早付款中列出的“发件人”地址)。你可以看到从faucet发送给你的和其他地址的测试ether。你看到的每笔交易都可能带给你更多的地址和交易。不久之后,你将迷失在相互关联的数据迷宫中。公共区块链包含大量的信息,所有这些都可以通过编程方式进行探索,我们将在未来的例子中看到。
世界计算机简介
我们已经创建了一个钱包,并且我们已经发送并接收了ether。到目前为止,我们已经将以太坊视为一种加密货币。但是以太坊代表了更多。事实上,加密货币功能是服务于以太坊作为世界计算机的功能; 一个去中心化的智能合约平台。以太旨在用于支付运行的 smart contracts,这是在称为_Ethereum虚拟机(EVM)_的模拟计算机上运行的计算机程序。
EVM是一个全球性的单例,这意味着它的运作方式就好像它是一个全球性的单实例计算机,无处不在。以太坊网络上的每个节点运行EVM的本地副本以验证合约执行,而以太坊区块链记录此世界计算机在处理交易和智能合约时变化的 状态。
外部所有账户(EOAs)和合约
我们在MetaMask钱包中创建的账户类型称为 Externally Owned Account(EOA) 。外部所有账户是那些拥有私人密钥的账户,它控制对资金或合约的访问。现在,你可能猜测还有另一种帐户,_合约_帐户。合约账户由以太坊区块链记录,由EVM执行的软件程序的逻辑所拥有(和控制)。
将来,所有以太坊钱包可能会作为以太坊合约运行,模糊了外部所有账户和合约账户之间的区别。但是永远保持的重要区别在于:人们通过EOA做出决定,而软件通过合约做出决定。
合约有一个地址,就像EOAs(钱包)一样。合约可以发送和接收ether,就像钱包一样。当交易目的地是合约地址时,它会导致该合约在EVM中_运行_,并将交易作为其输入。
除了ether之外,交易还可以包含_数据_,用于指示合约中要运行的特定方法以及传递给该方法的参数。通过这种方式,交易通过合约_调用_方法。最后,合约可以产生调用其他合约的交易,建立复杂的执行路径。其中一个典型的用法是合约A调用合约B,以便在合约A的用户之间保持共享状态。
在接下来的几节中,我们将编写我们的第一份合约。然后,我们将使用MetaMask钱包和测试ether在Ropsten测试网上创建,资助,使用该合约。
一个简单的合约:一个test ether faucet
以太坊有许多不同的高级语言,所有这些语言都可用于编写合约并生成EVM字节码。你可以阅读 high_level_languages 中许多最成功和有趣的内容。一种智能合约编程的主要高级语言:Solidity。本书的合著者Gavin Wood创建了Solidity,已经成为以太坊及以太坊外最广泛使用的语言。我们将使用Solidity编写我们的第一份合约。
作为我们的第一个例子,我们将编写一个控制_faucet_的合约。我们已经使用了faucent在Ropsten测试网络上获得测试ether。faucet是一件相对简单的 事情:它给任何地址发放ether,可以定期补充。你可以将faucet实现为由人类(或网络服务器)控制的钱包,但我们将编写一个实现faucet的Solidity 合约:
.Faucet.sol : A Solidity contract implementing a faucet
include::code/Solidity/Faucet.sol[]
这是一个非常简单的合约。这也是一个有缺陷的合约,显示了一些不良做法和安全漏洞。我们将通过检查后面章节中的所有缺陷来学习。但现在, 让我们逐行看下这个合约的作用,以及它是如何工作的。
第一行是注释
// Version of Solidity compiler this program was written for
注释用于人类阅读,不包含在可执行的EVM字节码中。我们通常将注释放在我们试图解释的代码之前,有时在同一行上。评论从两个正斜杠 //
开始。
从斜线和直到该行结束的所有内容都被视为空白行并被忽略。
下一行是我们的_真正的_合约开始的地方:
contract Faucet {
该行声明了一个合约对象,类似于JavaScript,Java或Cpass:[++]等其他面向对象语言中的 class
声明。合约的定义包含了大括号中的所有行
pass:[{}],它定义了 范围
,就像在其他许多编程语言中使用花括号一样。
接下来,我们声明faucet合约的第一个函数:
function withdraw(uint withdraw_amount) public {
函数名为 withdraw
,它接收一个无符号整数(uint
)名为 withdraw_amount
的参数。它被声明为 public
函数,意味着它可以被其
他合约调用。函数定义在花括号之间:
require(withdraw_amount <= 100000000000000000);
withdraw
方法的第一部分设置了取款限制。它使用内置的Solidity函数 require
来测试前提条件,即 withdraw_amount
小于或等于
100000000000000000 wei
,它是ether的基本单位(参见 ether_denominations ),等于0.1 ether。如果使用 withdraw_amount
大于该数量调用 withdraw
函数,则此处的 require
函数将导致合约执行停止并失败,并显示_异常_。
合约的这部分是我们faucet的主要逻辑。它通过设定提款限额来控制合约的资金流出。这是一个非常简单的控制,但可以让你看到可编程区块链的 强大功能:去中心化控制货币的软件。
msg.sender.transfer(withdraw_amount);
这里发生了一些有趣的事情。msg
对象是所有合约可以访问的输入之一。它代表触发执行此合约的交易。属性 sender
是交易的发件人地址。
函数 transfer
是一个内置函数,它将ether从合约传递到调用它的地址。从后往前读,表示 transfer
到触发此合约执行的 msg
的
sender
。transfer
函数将一个金额作为唯一的参数。我们传递之前声明为 withdraw
方法的参数的 withdraw_amount
值。
紧接着的一行是结束大括号,表示 withdraw
函数定义的结束。
下面我们又声明了一个函数:
function () public payable {}
此函数是所谓的_“fallback”_或_default_函数,如果合约的交易没有命名合约中任何已声明的功能或任何功能,或者不包含数据,则触发此函数。
合约可以有一个这样的默认功能(没有名字),它通常是接收ether的那个。这就是为什么它被定义为 public
和 payable
函数,这意味着
它可以接受合约中的ether。除了大括号中的空白定义 pass:[{}] 所指示的以外,它不会执行任何操作。如果我们进行一次向这个合约地址发送
ether的交易,就好像它是一个钱包一样,该函数将处理它。
在我们的默认函数下面是最后一个关闭花括号,它关闭了合约 faucet
的定义。就是这样!
编译faucet合约
现在我们已经有了我们的第一个示例合约,我们需要使用Solidity编译器将Solidity代码转换为EVM字节代码,以便它可以由EVM执行。
Solidity编译器是独立的可执行文件,作为不同框架的一部分,也捆绑在一个_Integrated Development Environment(IDE)_中。 为了简单起见,我们将使用一种更流行的IDE,称为Remix。
使用你的Chrome浏览器(使用我们之前安装的MetaMask钱包)导航到以下位置的Remix IDE:
https://remix.ethereum.org/ (opens new window)
当你第一次加载Remix时,它将以一个名为 ballot.sol
的示例合约开始。我们不需要这个,所以让我们关闭它,点击标签边的 x
:
Figure 10. Close the default example tab
现在,点击左侧工具栏中的圆形加号,添加一个新选项卡,命名新文件
Faucet.sol
: 
Figure 11. Click the plus sign to open a new tab 打开新选项卡后,复制并粘贴示例
Faucet.sol
: 
Figure 12. Copy the Faucet example code into the new tab
现在我们已将
Faucet.sol
合约加载到Remix IDE中,IDE将自动编译代码。如果一切顺利,你会看到一个绿色的放开,右边出现一个带
有“faucet”的绿色方块,在Compile选项卡下,确认编译成功:
Figure 13. Remix successfully compiles the Faucet.sol contract
如果出现问题,最可能的问题是Remix IDE正在使用与
0.4.19
版本不同的Solidity编译器。在这种情况下,我们的编译指示将阻止
Faucet.sol
编译。要更改编译器版本,请转到“Settings”选项卡,并重试。 Solidity编译器现在已将我们的Faucet.sol
编译为EVM字节码。如果你好奇,字节码如下所示:
PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH2 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH1 0xE5 DUP1 PUSH2 0x1D PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x60 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x3F JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x2E1A7D4D EQ PUSH1 0x41 JUMPI JUMPDEST STOP JUMPDEST CALLVALUE ISZERO PUSH1 0x4B JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH1 0x5F PUSH1 0x4 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP2 SWAP1 POP POP PUSH1 0x61 JUMP JUMPDEST STOP JUMPDEST PUSH8 0x16345785D8A0000 DUP2 GT ISZERO ISZERO ISZERO PUSH1 0x77 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST CALLER PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND PUSH2 0x8FC DUP3 SWAP1 DUP2 ISZERO MUL SWAP1 PUSH1 0x40 MLOAD PUSH1 0x0 PUSH1 0x40 MLOAD DUP1 DUP4 SUB DUP2 DUP6 DUP9 DUP9 CALL SWAP4 POP POP POP POP ISZERO ISZERO PUSH1 0xB6 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 PUSH9 0x13D1EA839A4438EF75 GASLIMIT CALLVALUE LOG4 0x5f PUSH24 0x7541F409787592C988A079407FB28B4AD000290000000000
你是不是很高兴使用像Solidity这样的高级语言,而不是直接在EVM字节码中编程?我也是!
在区块链上创建合约
我们有一个合约,已经将它编译成字节码。现在,我们需要在以太坊区块链上“登记”合约。我们将使用Ropsten测试网来测试我们的合约, 所以这就是我们想要记录的区块链。
在区块链上注册合约涉及创建一个特殊交易,其目标是地址0x0000000000000000000000000000000000000000,也称为_zero address_。 零地址是一个特殊的地址,告诉以太坊区块链你想注册一个合约。幸运的是,Remix IDE将为你处理所有这些交易并将交易发送给MetaMask。
首先,切换到“Run”选项卡,并在“Environment”下拉列表框中选择“Injected Web3”。这将Remix IDE连接到MetaMask钱包,并通过MetaMask 连接到Ropsten测试网络。一旦你这样做,你可以在Evironment下看到“Ropsten”。另外,在Account选择框中,它显示你的钱包的地址:

Figure 14. Remix IDE "Run" tab, with "Injected Web3" environment selected
在刚刚确认的“Run”设置下方,是Faucet合约,随时可以创建。点击“Create”或“Deploy“按钮:

Figure 15. Click the Create button in the Run tab
Remix IDE将构建特殊的“creation“交易,MetaMask会要求你批准它。从MetaMask中可以看到,合约创建交易没有ether,但它有258个字节(编译的合约) ,并且会消耗10个Gwei。点击“Submit”来批准:

Figure 16. MetaMask showing the contract creation transaction
现在,等待:合约在Ropsten上开采需要大约15到30秒的时间。Remix IDE似乎不会做太多,耐心等待。
合约创建后,它会显示在“运行”选项卡的底部:
Figure 17. The Faucet contract is ALIVE! 请注意,Faucet合约现在有自己的地址:Remix将其显示为
Faucet at 0x72e....c7829
。右边的小剪贴板符号允许你将合约地址复制到剪贴板中。
我们将在下一节中使用它。 与合约交互
让我们回顾一下我们迄今为止学到的东西:以太坊合约是控制货币的程序,运行在名为EVM的虚拟机内。它们是由一个特殊的交易创建的,该交易提交它 们的字节码以记录在区块链中。一旦他们在区块链上创建,他们就拥有一个以太坊地址,就像钱包一样。只要有人将交易发送到合约地址,它就会导致 合约在EVM中运行,并将交易作为其输入。发送到合约地址的交易可能包含以太网或数据或两者都有。如果它们含有ether,则将其“存入”合约余额。 如果它们包含数据,则数据可以在合约中指定一个命名函数并调用它,并将参数传递给该函数。
在区块浏览器中查看合约地址
现在,我们在区块链中登记了一份合约,我们可以看到它有一个以太坊地址。让我们在 ropsten.etherscan.io
区块浏览器中查看它,看看合约
是什么样子。通过点击名称旁边的剪贴板图标来复制合约的地址。

Figure 18. Copy the contract address from Remix
保持Remix打开在标签中,我们稍后会再回来。现在,将浏览器导航至
ropsten.etherscan.io
并将地址粘贴到搜索框中。你应该看到合约的以太坊
地址记录: 
Figure 19. View the Faucet contract address in the etherscan block explorer
为合约提供资金
现在,合约其历史上只有一笔交易:合约创建交易。如你所见,合约也没有ether(零余额)。这是因为我们没有在创建交易中向合约发送任何提示, 尽管我们可以提供。
让我们向合约发一些ether!你仍然应该在剪贴板中拥有合约的地址(如果没有,请从Remix再次复制)。打开MetaMask,然后向它发送1个ether, 就像任何其他以太坊地址一样:

Figure 20. Send 1 ether to the contract address
一分钟后,如果你刷新etherscan区块浏览器,它会向合约地址显示另一个交易,并更新1 ether的余额。
还记得我们的 Faucet.sol
代码中的未命名默认公共付费功能?它看起来像这样:
function () public payable {}
当你将交易发送到合约地址时,没有指定要调用哪个函数的数据,它将调用默认函数。由于我们将它声明为payable
,因此它接受1 ether并存入合约账户余额中。你的交易导致合约在EVM中运行,更新其余额。我们资助了我们的faucet!
从我们的合约中提取
接下来,让我们从faucet中提取一些资金。要提取,我们必须构造一个调用 withdraw
函数并将 withdraw_amount
参数传递给它的交易。为了保持现
在简单,Remix将为我们构建该交易,并且MetaMask将提交它以供我们批准。
返回到Remix选项卡并在“Run”选项卡下查看合约。你应该看到一个标记为 withdraw
的红色框,其中带有一个标记为 uint256 withdraw_amount
:
Figure 21. The withdraw function of Faucet.sol, in Remix
这是合约的Remix界面。它允许我们构造调用合约中定义的函数的交易。我们将输入 withdraw_amount
并点击 withdraw
按钮以生成交易。
首先,我们来看看 withdraw_amount
。我们要试着提取0.1 ether,这是我们合约允许的最高金额。请记住,以太坊中的所有货币值都以 wei
计价,而我们的 withdraw
函数预期 withdraw_amount
也以 wei
计价。我们想要的数量是0.1 ether,这是 100000000000000000 wei
(1后面跟着17个零)。
TIP
由于JavaScript的限制,Remix无法处理10^17这样大的数字。相反,我们用双引号括起来,让Remix以字符串的形式接收它,并将它作为 BigNumber
进行操作。如果我们不把它放在引号中,那么Remix IDE将无法处理它并显示“Error encoding arguments:Error:Assertion failed” 。
译者注:翻译此书时,已经支持直接输入数字
输入“100000000000000000”(带引号)到 withdraw_amount
框中,然后单击 withdraw
按钮:

Figure 22. Click "withdraw" in Remix to create a withdrawal transaction MetaMask将弹出一个交易窗口供你批准。点击“Submit”将你的提款通知发送至合约:

Figure 23. MetaMask transaction to call the withdraw function
等一下,然后重新加载
etherscan
区块浏览器以查看在ether合约地址历史记录中反映的交易: 
Figure 24. Etherscan shows the transaction calling the withdraw function 我们现在看到一个新的交易,其中合约地址是目标地址,0 ether。合约余额已经改变,现在是0.9 ether,因为它按要求给了我们0.1 ether。但是我们在合约地址历史记录中看不到“OUT”交易。
提款的交易在哪里?合约的地址历史记录页面中出现了一个名为“内部交易”的新选项卡。由于0.1 ether传输源于合约代码,因此它是一个内部交易(也称为_message_)。点击“内部交易”标签查看:

Figure 25. Etherscan shows the internal transaction transferring ether out from the contract
这个“内部交易”是由合约在这行代码中发送的(
Faucet.sol
的 withdraw
方法) msg.sender.transfer(withdraw_amount);
回顾一下:我们从MetaMask钱包发送了一个包含数据指令的交易,以 0.1 ether 的withdraw_amount
参数调用 withdraw
函数。该交易导致合约
在EVM内部运行。当EVM运行faucet合约的 withdraw
功能时,首先它调用require
函数并验证我们的金额小于或等于最大允许提款0.1 ether。
然后它调用 transfer
函数向我们发送ether。运行 transfer
函数生成一个内部交易,从合约的余额中将0.1以太币存入我们的钱包地址。
这就是 etherscan
中“内部交易”标签中显示的内容。
总结
本章中,我们使用MetaMask创建了一个钱包,并且使用Ropsten测试网络上的一个faucet为它充值。我们收到了发送到钱包以太坊地址的ether。 然后我们把ether发送到faucet的以太坊地址。
接下来,我们在Solidity中写了一个faucet合约。使用Remix IDE将合约编译为EVM字节码。使用Remix进行交易,并在Ropsten区块链上登记
faucet合约。一旦登记,faucet合约有一个以太坊地址,我们发送一些ether。最后,我们构建了一个交易来调用 withdraw
函数,并成功
请求了0.1 ether。该合约检查了我们的请求,发送给我们0.1 ether并进行内部交易。
可能看起来不多,但我们刚刚成功地与控制去中心化世界计算机上资金的软件进行了交互。
我们将在“智能合约”中做更多的智能合约编程,并了解最佳实践和安全考虑。
# 第三章 以太坊客户端
# 以太坊客户端
以太坊客户端是实现以太坊规范并通过对等网络与其他以太坊客户端进行通信的软件应用程序。不同的以太坊客户端如果符 合参考规范和标准化通信协议,就可以互操作。虽然这些不同的客户端由不同的团队和不同的编程语言实现,但他们 都“说”相同的协议并遵循相同的规则。
以太坊是一个_open source_项目,源代码可在开放(LGPL v3.0)许可下使用,可免费下载并用于任何目的。开源意味着不 仅仅是免费使用。这也意味着以太坊由一个开放的志愿者社区开发,任何人都可以修改。
以太坊由名为“黄皮书”的正式规范定义。这与比特币相反,比特币没有任何正式的定义。比特币的“规范”是比特币核心的参 考实现,以太坊的规范定义在一篇结合了英文和数学的(正式的)规范的论文中。这个正式的规范,除了各种以太坊改进 建议之外,还定义了以太坊客户端的标准行为。随着对以太坊的重大改变,黄皮书会定期更新。
作为以太坊明确的正式规范的结果,以太坊客户端有许多独立开发的,可互操作的软件实现。以太坊在网络上运行的实现方 式比任何其他区块链都多。
# 以太坊网络
存在各种基于以太坊的网络,这些网络很大程度上符合以太坊“黄皮书”中定义的正式规范,但它们可能或不能互操作。
在这些以太坊网络中有:Ethereum,Ethereum Classic,Ella,Expanse,Ubiq,Musicoin等等。虽然大多数在协议级别上兼 容,但这些网络通常具需要以太坊客户端软件维护人员进行微小更改以支持每个网络的功能或属性。因此,并非以太坊客 户端软件的每个版本都可以在每个以太坊区块链上运行。
目前,以六种不同语言编写的以太坊协议有六个主要实现:Go(Geth),Rust(parity),C ++(cpp-ethereum) ,Python(pyethereum),Scala(mantis)和Java(harmony)。
在本节中,我们将看看两个最常见的客户,Geth和Parity。我们将学习如何使用每个客户端启动一个节点,并探索他们的 一些命令行和应用程序编程接口(API)。
# 我应该运行一个完整的节点吗?
区块链的健康,弹性和抗审查取决于拥有有多少独立运营和地理上分散的完整节点。每个完整节点都可以帮助其他新节点获 取块数据以引导其操作,并为运营商提供对所有交易和合约的权威和独立验证。
但是,运行完整的节点会导致硬件资源和带宽的巨大成本。完整的节点必须下载超过80GB的数据(截至2018年4月;取决于
客户端)并将其存储在本地硬盘上。随着新的交易和区块的添加,这种数据负担每天都会迅速增加。完整节点的硬件要求
中有关于此主题的更多信息。
在以太坊开发中,运行在活跃网络(主网)上的完整节点不是必需的。你可以使用_testnet_节点(它存储小型公共测试区块
链的副本),或本地私有区块链(参见 ganache
),或服务提供商提供的基于云的以太坊客户端(参见 infura
),做几乎
任何事。
你还可以选择运行轻量级客户端,该客户端不会存储区块链的本地副本或验证块和交易。这些客户端提供钱包的功能,并可 以创建和广播交易。
轻量级客户端可用于连接到现有网络,例如你自己的完整节点,公共区块链,公开或许可的(PoA)测试网或私有本地区块链。 在实践中,你可能会使用轻量级客户端,如MetaMask,Emerald Wallet,MyEtherWallet或MyCrypto作为在所有不同节 点选项之间切换的便捷方式。
尽管存在一些差异,术语“轻量级客户端”和“钱包”可以互换使用。通常,轻量级客户端除了提供钱包的交易功能外,还提供 API(如web3js API)。
不要将以太坊中轻量级钱包的概念与比特币中简化支付验证(SPV)客户端的概念混淆。SPV客户验证区块头并使用merkle 证明来验证区块链中是否包含交易。以太坊轻量级客户端通常不验证区块头或交易。他们完全信任由第三方运营的完整客户 端,让他们通过RPC访问区块链。
# 完整节点的优点和缺点
选择运行一个完整的节点可以帮助各种基于以太坊的网络,但也会给你带来一些温和的或适中的成本。我们来看看一些优点和缺点。
优点:
- 支持基于以太坊的网络的弹性和抗审查。
- 权威性验证所有交易。
- 可以与公共区块链上的任何合约进行交互(无需中介)。
- 如有必要,可以离线查询(只读)区块链状态(账户,合约等)。
- 可以在不让第三方知道你正在读取的信息的情况下查询区块链。
- 可以直接将自己的合约部署到公共区块链中(无需中介)。
缺点:
- 需要大量且不断增长的硬件和带宽资源。
- 需要几个小时或几天才能完成第一次初始下载的同步。
- 必须维护,升级并保持联机才能保持同步。
# 公共测试网的优点和缺点
无论你是否选择运行完整节点,你可能都需要运行公共testnet节点。我们来看看使用公共测试网的一些优点和缺点。
优点:
- 测试网络节点需要同步并存储少得多的数据,根据网络大小约为10GB(截至2018年4月)。
- 测试网络节点可以在几个小时内完全同步。
- 部署合约或进行交易需要测试ether,它没有价值,可以从几个“faucet”免费获得。
- Testnets是与其他许多用户和合约共享的区块链,运行“live”。
缺点:
- 你不能在测试网上使用“真实”的钱,它以测试ether运转。
- 因此,你无法针对真正对手进行安全性测试,因为没有任何风险。
- 公共区块链的某些方面无法在testnet上真实地测试。例如,交易费虽然是发送交易所必需的,但由于gas是免费的,因 此不需要在测试网上考虑。测试网不会像公共网络那样经历网络拥塞。
# 本地实例(TestRPC)的优点和缺点
对于许多测试目的,最好的选择是使用 testrpc
节点启动一个实例私有区块链。TestRPC创建一个本地私有区块链,你可以与
之交互,而无需任何其他参与者。它分享了公共测试网的许多优点和缺点,但也有一些差异。
优点:
- 不同步,磁盘上几乎没有数据。你自己挖掘第一块。
- 无需测试ether,你可以将挖矿奖励“奖励”给自己,用于测试。
- 没有其他用户,只有你。
- 没有其他合约,只有你启动后部署的合约。
缺点:
- 没有其他用户意味着它不像公共区块链一样。没有交易空间或交易排序的竞争。
- 除你之外没有矿工意味着采矿更具可预测性,因此你无法测试公开区块链上发生的一些情况。
- 没有其他合约意味着你必须部署所有你想测试的内容,包括依赖项和合约库。
- 你不能重新创建一些公共合约及其地址来测试一些场景(例如DAO合约)。
# 运行以太坊客户端
如果你有时间和资源,你应该尝试运行一个完整的节点,即使只是为了更多地了解这个过程。在接下来的几节中,我们将 下载,编译和运行以太坊客户Go-Ethereum(Geth)和Parity。这需要熟悉在操作系统上使用命令行界面。无论你选择 将它们作为完整节点,作为testnet节点还是作为本地私有区块链的客户端运行,都值得安装这些客户端。
# 完整节点的硬件要求
在我们开始之前,你应该确保你有一台具有足够资源的计算机来运行以太坊完整节点。你将需要至少80GB的磁盘空间来存 储以太坊区块链的完整副本。如果你还想在以太坊测试网上运行完整节点,则至少需要额外的15GB。下载80GB的区块链 数据可能需要很长时间,因此建议你使用快速的Internet连接。
同步以太坊区块链是非常密集的输入输出(I/O)。最好有一个固态硬盘(SSD)。如果你有机械硬盘驱动器(HDD), 则至少需要8GB的RAM能用作缓存。否则,你可能会发现你的系统速度太慢,无法完全保持同步。
最低要求:
- 2核心CPU。
- 固态硬盘(SSD),至少80GB可用空间。
- 最小4GB内存,如果你使用HDD而不是SSD,则至少8GB。
- 8+ MBit/sec下载速度的互联网。
这些是同步基于以太坊的区块链的完整(但已修剪)副本的最低要求。
在编写本文时(2018年4月),Parity代码库的资源往往更轻,如果你使用有限的硬件运行,那么使用Parity可能会看到最好 的结果。
如果你想在合理的时间内同步并存储我们在本书中讨论的所有开发工具,库,客户端和区块链,你将需要一台功能更强大的计 算机。
推荐规格:
- 4个以上核心的快速CPU。
- 16GB+ RAM。
- 至少有500GB可用空间的快速SSD。
- 25+ MBit/sec下载速度的互联网。
很难预测区块链的大小会增加多快,以及何时需要更多的磁盘空间,所以建议你在开始同步之前检查区块链的最新大小。
以太坊: https://bitinfocharts.com/ethereum/ (opens new window)
以太坊经典: https://bitinfocharts.com/ethereum%20classic/ (opens new window)
# 构建和运行客户端(节点)的软件要求
本节介绍Geth和Parity客户端软件。并假设你正在使用类Unix的命令行环境。这些示例显示了在运行Bash shell(命令行执行 环境)的Ubuntu Linux操作系统上输入的输出和命令。
通常,每个区块链都有自己的Geth版本,而Parity支持多个以太坊区块链(Ethereum,Ethereum Classic,Ellaism, Expanse,Musicoin)。
在我们开始之前,我们可能需要满足一些先决条件。如果你从未在你当前使用的计算机上进行任何软件开发,则可能需要安装
一些基本工具。对于以下示例,你需要安装 git
,源代码管理系统; Golang
,Go编程语言和标准库; 和Rust,一种系统编程语言。
可以按照以下说明安装Git: https://git-scm.com/ (opens new window)
可以按照以下说明安装Go: https://golang.org/ (opens new window)
NOTE
Geth的要求各不相同,但如果你坚持使用Go版本1.10或更高版本,你应该能够编译你想要的任何版本的Geth。
当然,你应该总是参考你选择的Geth的文档。
如果安装在你的操作系统上的Golang版本或者从系统的软件包管理器中获得的版本远远早于1.10,请将其删除
并从golang.org安装最新版本。
Rust可以按照以下说明进行安装:
https://www.rustup.rs/ (opens new window)
NOTE
Parity需要Rust版本1.24或更高版本。
Parity还需要一些软件库,例如OpenSSL和libudev。要在Linux(Debian)兼容系统上安装,请执行以下操作:
$ sudo apt-get install openssl libssl-dev libudev-dev
对于其他操作系统,请使用操作系统的软件包管理器或遵循Wiki说明(https://github.com/paritytech/parity/wiki/Setup (opens new window))来 安装所需的库。
现在你已经安装了 git
,golang
,rust
和必要的库,让我们开始工作吧!
# Parity
Parity是完整节点以太坊客户端和DApp浏览器的实现。Parity是由Rust从头开始编写的,系统编程语言是为了构建一个模块 化,安全和可扩展的以太坊客户端。Parity由英国公司Parity Tech开发,并以GPLv3开源许可证发布。
NOTE
披露:本书的作者之一Gavin Wood是Parity Tech的创始人,并撰写了大部分Parity客户端。Parity代表了约28%的以太坊 客户端。
要安装Parity,你可以使用Rust包管理器cargo
或从GitHub下载源代码。软件包管理器也下载源代码,所以两种选择之间
没有太大区别。在下一节中,我们将向你展示如何自己下载和编译Parity。
# 安装 Parity
Parity Wiki提供了在不同环境和容器中构建Parity的说明:
https://github.com/paritytech/parity/wiki/Setup (opens new window)
我们将从源代码构建奇偶校验。这假定你已经使用 rustup
安装了Rust(见 [构建和运行客户端(节点)的软件要求])。
首先,让我们从GitHub获取源代码:
$ git clone https://github.com/paritytech/parity
现在,我们转到parity
目录并使用cargo
构建可执行文件:
$ cd parity
$ cargo build
2
如果一切顺利,你应该看到如下所示的内容:
$ cargo build
Updating git repository `https://github.com/paritytech/js-precompiled.git`
Downloading log v0.3.7
Downloading isatty v0.1.1
Downloading regex v0.2.1
[...]
Compiling parity-ipfs-api v1.7.0
Compiling parity-rpc v1.7.0
Compiling parity-rpc-client v1.4.0
Compiling rpc-cli v1.4.0 (file:///home/aantonop/Dev/parity/rpc_cli)
Finished dev [unoptimized + debuginfo] target(s) in 479.12 secs
$
2
3
4
5
6
7
8
9
10
11
12
13
14
让我们通过调用--version
选项来运行parity
以查看它是否已安装:
$ parity --version
Parity
version Parity/v1.7.0-unstable-02edc95-20170623/x86_64-linux-gnu/rustc1.18.0
Copyright 2015, 2016, 2017 Parity Technologies (UK) Ltd
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
By Wood/Paronyan/Kotewicz/Drwięga/Volf
Habermeier/Czaban/Greeff/Gotchac/Redmann
$
2
3
4
5
6
7
8
9
10
11
现在已安装了Parity,我们可以同步区块链并开始使用一些基本的命令行选项。
# Go-Ethereum (Geth)
Geth是Go语言实现的,它被积极开发并被视为以太坊客户端的“官方”实现。通常情况下,每个基于以太坊的区块链都会有自己的Geth实现。如果你正在运行Geth,那么你需要确保使用以下某个存储库链接为区块链获取正确的版本。
# 版本库链接
Ethereum: https://github.com/ethereum/go-ethereum (or https://geth.ethereum.org/) (opens new window)
Ethereum Classic: https://github.com/ethereumproject/go-ethereum (opens new window)
Ellaism: https://github.com/ellaism/go-ellaism (opens new window)
Expanse: https://github.com/expanse-org/go-expanse (opens new window)
Musicoin: https://github.com/Musicoin/go-musicoin (opens new window)
Ubiq: https://github.com/ubiq/go-ubiq (opens new window)
NOTE
你也可以跳过这些说明并为你选择的平台安装预编译的二进制文件。预编译的版本安装起来更容易,可以在上面版本库的“版本” 部分找到。但是,你可以通过自己下载和编译软件来了解更多信息。
# 克隆存储库
我们的第一步是克隆git仓库,以获得源代码的副本。
要创建此存储库的本地克隆,请使用 git
命令,如下所示,在你的主目录或用于开发的任何目录下:
$ git clone <Repository Link>
在将存储库复制到本地系统时,你应该看到进度报告:
Cloning into 'go-ethereum'...
remote: Counting objects: 62587, done.
remote: Compressing objects: 100% (26/26), done.
remote: Total 62587 (delta 10), reused 13 (delta 4), pack-reused 62557
Receiving objects: 100% (62587/62587), 84.51 MiB | 1.40 MiB/s, done.
Resolving deltas: 100% (41554/41554), done.
Checking connectivity... done.
2
3
4
5
6
7
现在我们有了Geth的本地副本,我们可以为我们的平台编译一个可执行文件。
# 从源代码构建Geth
要构建Geth,切换到下载源代码的目录并使用 make
命令:
$ cd go-ethereum
$ make geth
2
如果一切顺利,你将看到Go编译器构建每个组件,直到它生成geth
可执行文件:
build/env.sh go run build/ci.go install ./cmd/geth
>>> /usr/local/go/bin/go install -ldflags -X main.gitCommit=58a1e13e6dd7f52a1d5e67bee47d23fd6cfdee5c -v ./cmd/geth
github.com/ethereum/go-ethereum/common/hexutil
github.com/ethereum/go-ethereum/common/math
github.com/ethereum/go-ethereum/crypto/sha3
github.com/ethereum/go-ethereum/rlp
github.com/ethereum/go-ethereum/crypto/secp256k1
github.com/ethereum/go-ethereum/common
[...]
github.com/ethereum/go-ethereum/cmd/utils
github.com/ethereum/go-ethereum/cmd/geth
Done building.
Run "build/bin/geth" to launch geth.
$
2
3
4
5
6
7
8
9
10
11
12
13
14
让我们在停止并更改它的配置之前运行 geth
以确保它工作:
$ ./build/bin/geth version
Geth
Version: 1.6.6-unstable
Git Commit: 58a1e13e6dd7f52a1d5e67bee47d23fd6cfdee5c
Architecture: amd64
Protocol Versions: [63 62]
Network Id: 1
Go Version: go1.8.3
Operating System: linux
GOPATH=/usr/local/src/gocode/
GOROOT=/usr/local/go
2
3
4
5
6
7
8
9
10
11
12
13
你的 geth version
命令可能会稍微不同,但你应该看到类似上面的版本报告。
最后,我们可能希望将 geth
命令复制到操作系统的应用程序目录(或命令行执行路径上的目录)。在Linux上,我们使用
以下命令:
$ sudo cp ./build/bin/geth /usr/local/bin
先不要开始运行 geth
,因为它会以“缓慢的方式”开始将区块链同步,这将花费太长的时间(几周)。[基于以太坊的区块链首次同步] 解释
了以太坊区块链的初始同步带来的挑战。
[[first_sync]]
# 基于以太坊的区块链首次同步
通常,在同步以太坊区块链时,你的客户端将下载并验证自创世区块以来的每个区块和每个交易。
虽然可以通过这种方式完整同步区块链,但同步会花费很长时间并且对计算资源要求较高(RAM更多,存储速度更快)。
许多基于以太坊的区块链在2016年底遭受了拒绝服务(DoS)攻击。受此攻击影响的区块链在进行完全同步时倾向于缓慢同步。
例如,在以太坊中,新客户端在到达区块2,283,397之前会进展迅速。该块在2016年9月18日开采,标志着DoS攻击的开始。从 这个区块到2,700,031区块(2016年11月26日),交易验证变得非常缓慢,内存密集并且I/O密集。这导致每块的验证时间超过 1分钟。以太坊使用硬分叉实施了一系列升级,以解决在拒绝服务中被利用的底层漏洞。这些升级还通过删除由垃圾邮件交易创 建的大约2000万个空帐户来清理区块链。[1]
如果你正在使用完整验证进行同步,则客户端会放慢速度并可能需要几天或更长时间才能验证受此DoS攻击影响的任何块。
大多数以太坊客户端包括一个选项,可以执行“快速”同步,跳过交易的完整验证,同步到区块链的顶端后,再恢复完整验证。
对于Geth,启用快速同步的选项通常称为 --fast
。你可能需要参考你选择的以太坊链的具体说明。
对于Parity,较旧版本(<1.6),该选项为 --warp
,较新版本(>=1.6)上默认启用(无需设置配置选项)。
NOTE
Geth和Parity只能在空的区块数据库启动时进行快速同步。如果你已经开始没有“快速”模式的同步,则Geth和Parity无法切换。 删除区块链数据目录并从头开始“快速”同步比继续完整验证同步更快。删除区块链数据时请小心不要删除任何钱包!
# JSON-RPC接口
以太坊客户端提供应用程序编程接口(API)和一组远程过程调用(RPC)命令,这些命令被编码为JavaScript对象表示法 (JSON)。这被称为_JSON-RPC API_。本质上,JSON-RPC API是一个接口,允许我们将使用以太坊客户端的程序作为 _gateway_编写到以太坊网络和区块链中。
通常,RPC接口作为端口8545
上的HTTP服务提供。出于安全原因,默认情况下,它仅受限于从本地主机(你自己的计算机
的IP地址为127.0.0.1
)接受连接。
要访问JSON-RPC API,可以使用专门的库,用你选择的编程语言编写,它提供与每个可用的RPC命令相对应的“桩(stub)”
函数调用。或者,你可以手动构建HTTP请求并发送/接收JSON编码的请求。你甚至可以使用通用命令行HTTP客户端(如 curl
)
来调用RPC接口。让我们尝试一下(确保你已经配置并运行了Geth):
Using curl to call the web3_clientVersion function over JSON-RPC
$ curl -X POST -H "Content-Type: application/json" --data \
'{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' \
http://localhost:8545
{"jsonrpc":"2.0","id":1,
"result":"Geth/v1.8.0-unstable-02aeb3d7/linux-amd64/go1.8.3"}
2
3
4
5
6
在这个例子中,我们使用 curl
建立一个HTTP连接来访问 http://localhost:8545
。我们已经运行了 geth
,它将
JSON-RPC API作为端口8545上的HTTP服务提供。我们指示 curl
使用HTTP POST
命令并将内容标识为
Content-Type: application/json
。最后,我们传递一个JSON编码的请求作为我们HTTP请求的data
部分。
我们的大多数命令行只是设置 curl
来正确地建立HTTP连接。有趣的部分是我们发布的实际的JSON-RPC命令:
{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":4192}
JSON-RPC请求根据JSON-RPC 2.0规范格式化,你可以在这里看到: http://www.jsonrpc.org/specification (opens new window)
每个请求包含4个元素:
jsonrpc
JSON-RPC协议的版本。这_必须_是“2.0”。
method
要调用的方法的名称。
params
一个结构化值,用于保存在调用方法期间要使用的参数值。该元素可以省略。
id
由客户端建立的标识符,必须包含字符串,数字或NULL值(如果包含)。如果包含,服务器必须在Response对象中使用
相同的值进行回复。该元素用于关联两个对象之间的上下文。
[TIP]
TIP
id
参数主要用于在单个JSON-RPC调用中进行多个请求的情况,这种做法称为_批处理_。批处理用于避免每个请求的新
HTTP和TCP连接的开销。例如,在以太坊环境中,如果我们想要在一个HTTP连接中检索数千个交易,我们将使用批处理。
批处理时,为每个请求设置不同的 id
,然后将其与来自JSON-RPC服务器的每个响应中的id
进行匹配。实现这个最
简单的方法是维护一个计数器并为每个请求增加值。
The response we receive is:
{"jsonrpc":"2.0","id":4192,
"result":"Geth/v1.8.0-unstable-02aeb3d7/linux-amd64/go1.8.3"}
2
这告诉我们JSON-RPC API由Geth客户端版本1.8.0提供服务。
让我们尝试一些更有趣的事情。在下一个例子中,我们要求JSON-RPC API获取当前的gas价格,以wei为单位:
$ curl -X POST -H "Content-Type: application/json" --data \
'{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":4213}' \
http://localhost:8545
{"jsonrpc":"2.0","id":4213,"result":"0x430e23400"}
2
3
4
5
响应 0x430e23400
告诉我们,当前的gas价格是1.8wei(gigawei或十亿wei)。
https://github.com/ethereum/wiki/wiki/JSON-RPC (opens new window)
# Parity的Geth兼容模式
有一个特殊的“Geth兼容模式”,它提供了一个与geth
相同的JSON-RPC API。要在Geth兼容模式下运行奇偶校验,请使用
--geth
开关:
$ parity --geth
# 轻量级以太坊客户
轻量级客户端提供了完整客户端的一部分功能。他们不存储完整的以太坊区块链,因此它们的启动速度更快,所需的数据存储量 也更少。
轻量级客户端提供以下一项或多项功能:
- 管理钱包中的私钥和以太坊地址。
- 创建,签署和广播交易。
- 使用数据与智能合约进行交互。
- 浏览并与DApps交互。
- 提供到区块浏览器等外部服务的链接。
- 转换ether单位并从外部来源检索汇率。
- 将web3实例作为JavaScript对象注入到Web浏览器中。
- 使用另一个客户端提供/注入浏览器的web3实例。
- 在本地或远程以太网节点上访问RPC服务。
一些轻量级客户端(例如移动(智能手机)钱包)仅提供基本的钱包功能。其他轻量级客户端是完全开发的DApp浏览器。轻量 级客户端通常提供完整节点以太坊客户端的某些功能,而无需同步以太坊区块链的本地副本。
我们来看看一些最受欢迎的轻量级客户端及其提供的功能。
# 移动(智能手机)钱包
所有的移动钱包都是轻量级的客户端,因为智能手机没有足够的资源来运行完整的以太坊客户端。
流行的移动钱包包括Jaxx,Status和Trust Wallet。我们列举这些作为流行手机钱包的例子(不是对这些钱包的安全或功能的 认可)。
Jaxx :: 基于BIP39助记种子的多币种手机钱包,支持比特币,莱特币,以太坊,以太坊经典,ZCash,各种ERC20代币和许多 其他货币。Jaxx可在Android,iOS上作为浏览器插件钱包使用,桌面钱包可用于各种操作系统。可以在https://jaxx.io (opens new window)找到它。
Status:: 移动钱包和DApp浏览器,支持各种代币和流行的DApps。适用于iOS和Android智能手机。可以在 https://status.im找到它。
Trust Wallet:: 支持ERC20和ERC223代币的移动以太坊,以太坊经典钱包。Trust Wallet适用于iOS和Android智能手机。 可以在https://trustwalletapp.com/ (opens new window)找到它。
Cipher Browser:: 全功能的启用以太坊的移动DApp浏览器和钱包。允许与以太坊应用程序和代币集成。 可以在https://www.cipherbrowser.com (opens new window)找到它
# 浏览器钱包
各种钱包和DApp浏览器可用作浏览器的插件或扩展,例如Chrome和Firefox:运行在浏览器内的轻量级客户端。
一些比较流行的是MetaMask,Jaxx和MyEtherWallet/MyCrypto。
# MetaMask
MetaMask 在 [intro] 中介绍,它是一个多功能的基于浏览器的钱包,RPC客户端和基本合约浏览器。它可用于Chrome, Firefox,Opera和Brave Browser。在以下位置找到MetaMask:
https://metamask.io (opens new window)
乍一看,MetaMask是一款基于浏览器的钱包。但是,与其他浏览器钱包不同,MetaMask将web3实例注入浏览器,作为连接到 各种以太坊区块链(例如mainnet,Ropsten testnet,Kovan testnet,本地RPC节点等)的RPC客户端。能够注入web3实 例并充当外部RPC服务的入口,使MetaMask成为开发人员和用户非常强大的工具。例如,它可以与MyEtherWallet或MyCrypto 相结合,充当这些工具的web3提供者和RPC网关。
# Jaxx
在 [移动(智能手机)钱包] 中作为移动钱包介绍的Jaxx也可用作Chrome和Firefox扩展。可以在这里找到:
https://jaxx.io (opens new window)
# MyEtherWallet (MEW)
MyEtherWallet是一款基于浏览器的JavaScript轻量级客户端,提供:
- 在JavaScript中运行的软件钱包。
- 通往诸如Trezor和Ledger等流行硬件钱包的桥梁。
- 一个web3界面,可以连接到另一个客户端注入的web3实例(例如MetaMask)。
- 可以连接到以太坊完整客户端的RPC客户端。
- 给定合约地址和应用程序二进制接口(ABI),可以与智能合约交互的基本接口。
MyEtherWallet对于测试和作为硬件钱包界面非常有用。它不应该被用作主要的软件钱包,因为它在浏览器环境中会受到威胁, 并且不是一个安全的密钥存储系统。
访问MyEtherWallet和其他基于浏览器的JavaScript钱包时,你必须非常小心,因为它们经常是钓鱼攻击的目标。始终使用 书签而不是搜索引擎或链接访问正确的网址。MyEtherWallet可以在以下网址找到:
https://MyEtherWallet.com (opens new window)
# MyCrypto
就在本书第一版出版之前,MyEtherWallet项目分为由两个独立开发团队主导的竞争实现:一个“分叉”,就像在开源开发中 所称的那样。这两个项目被称为MyEtherWallet(原始品牌)和MyCrypto。在拆分时,MyCrypto提供与MyEtherWallet相 同的功能。由于两个开发团队采取不同的目标和优先事项,这两个项目可能会出现分歧。
与MyEtherWallet一样,在浏览器中访问MyCrypto时必须非常小心。始终使用书签,或者非常小心地输入URL(然后将其书签 以备将来使用)。
MyCrypto可以在以下网址找到:
https://MyCrypto.com (opens new window)
# Mist
Mist是以太坊基金会创建的第一个启用以太坊的浏览器。它还包含一个基于浏览器的钱包,这是有史以来第一个实现ERC20代币 标准的(Fabian Vogelsteller,ERC20的作者也是Mist的主要开发人员)。Mist也是第一个引入camelCase校验和的软件包 (EIP-155,参见 [eip-155] )。Mist运行一个完整的节点,并提供完整的DApp浏览器,支持基于Swarm的存储和ENS地址。 可以在以下网址找到:
https://github.com/ethereum/mist (opens new window)
# References
- [[[1]]] EIP-161: http://eips.ethereum.org/EIPS/eip-161 (opens new window)
# 第四章 以太坊测试网
# 以太坊测试网(Testnets)
# 什么是测试网?
测试网络(简称testnet)用于模拟以太网主网的行为。有一些公开的测试网络可以替代以太坊区块链。这些网络上的货币毫无 价值,但它们仍然很有用,因为合约和协议变更的功能可以在不中断以太网主网或使用真实货币的情况下进行测试。当主网 (简称mainnet)即将包含对以太坊协议的任何重大改变时,其测试主要在这些测试网络上完成。这些测试网络也被大量开发 人员用于在部署到主网之前测试应用程序。
# 使用 Testnets
你可以连接到公共可用的测试网络或创建你自己的私人测试网络。首先,让我们使用公共测试网来更简单地起步。要使用公共 测试网络,需要一些测试网络以及到该网络的连接。对于testnet ether,使用“faucet”,faucet缓慢地分配测试ether, 向任何询问的人“滴送”少量ether。要连接到一个测试网络,你需要一个以太坊客户端,完整的客户端,比如geth,或者完整 的客户端的网关,比如MetaMask。
# 获取测试以太网
由于测试网不以真正的金钱运作,矿工保护测试网的动机很弱。因此,测试网必须保护自己免受滥用和攻击。因此,为这些测试 网创建了水龙头,以受控的方式向开发人员分发免费的测试ether(大多数faucet每隔几秒左右"滴注"ether)。这种以太网的 受控分配可防止用户滥用链,因为提供有限的ether供应可防止他们向链中写入过多内容或执行太多交易。另外,一些testnets 已经实施了认证证明(Proof of Authentication)方案,使用faucet需要具有适当社交媒体网站的认证的凭证。
# 连接到Testnets
# Metamask
Metamask完全支持Ropsten,Kovan和Rinkeby测试网,但也可以连接到其他测试网和本地网。在Metamask中,只需单击 “main network”下拉菜单,即可切换网络。MetaMask还提供了一个“buy”测试ether的选项,该选项将你引导至你可以 请求免费测试以太网的faucet。如果使用Ropsten测试网,则可以从Ropsten测试faucet服务中获取ether。你可以从此 页面访问此faucet。它需要Metamask扩展才能工作。https://faucet.metamask.io/ (opens new window)
# Infura
当MetaMask连接到测试网络时,它使用Infura服务提供商来访问JSON-RPC接口。Infura诞生的目的是为ConsenSys内部项目 提供稳定可靠的RPC访问。除了JSON-RPC API之外,Infura还提供REST(表述性状态转移)API,IPFS(星际文件系统,即去 中心化存储)API和Websockets(即流式传输)API。
Infura为Ethereum主网,Ropsten,Kovan,Rinkeby和INFURAnet(用于Infura的定制测试网络)提供网关API。
要通过MetaMask使用Infura进行较低级别的活动,你不需要账户。要直接使用API,你需要注册一个账户并使用Infura提供的 API密钥。
有关Infura的更多信息,请访问:
https://infura.io/ (opens new window)
# Remix集成开发环境(IDE)
Remix IDE可用于在主网和测试网上部署和交互智能合约,包括Ropsten,Rinkeby和Kovan(Web3提供者使用Infura地址和
API密钥或通过Injected Web3使用MetaMask中选择的网络)和Ganache( Web3提供端点http://localhost:8545
)
https://github.com/ethereum/remix/blob/master/docs/run_tab.rst (opens new window) https://medium.com/swlh/deploy-smart-contracts-on-ropsten-testnet-through-ethereum-remix-233cd1494b4b (opens new window)
# Geth
Geth本身支持Ropsten和Rinkeby网络。要连接到Ropsten网络,请使用命令行参数:
geth --testnet
这将开始同步Ropsten区块链。名为 testnet
的新目录将在你的主Ethereum数据目录中创建。一个 keystore
目录将在 testnet
内部创建,并将存储你的testnet帐户的私钥。在撰写本文时,Ropsten区块链比以太坊
主区块链小得多:大约14GB的数据。由于测试网需要的资源较少,因此首先在测试网上设置并测试你的代码会更简单。
与testnet的交互与mainnet类似。你可以使用控制台启动Geth testnet,方法是运行:
geth --testnet console
这使得执行操作成为可能,例如开设新账户,检查余额,检查其他以太坊地址的余额等。
在Geth控制台之外运行时,只需将--testnet
参数添加到命令行指令中,就可以执行类似于在主网上执行的操作。作为
列举所有可用的testnet帐户及其地址的示例,请运行:
geth --testnet account list
TIP
虽然小得多,但测试网仍需要一些时间才能完全同步。
你可以通过在geth交互式控制台中运行以下命令来检查geth是否已完成同步测试网络:
eth.getBlock("latest").number
一旦你的testnet节点完全同步,这应该返回一个非0的数字。你可以将该编号与已知的testnet区块浏览器中的最新块 进行比较,例如https://ropsten.etherscan.io/ (opens new window)
同样,要连接到Rinkeby测试网络,请使用命令行参数:
geth --rinkeby
# Parity
Parity客户端支持Ropsten和Kovan测试网络。你可以用chain
参数选择你要连接的网络。例如,要同步Ropsten测试网络:
parity --chain ropsten
同样,要同步Kovan测试网络,请使用:
parity --chain kovan
# 深入以太坊Testnets
在这个阶段你可能会想:“我明白我为什么要使用测试网络,但为什么会有这么多呢?”
https://www.ethnews.com/ropsten-to-kovan-to-rinkeby-ethereums-testnet-troubles (opens new window)
# 工作量证明(挖矿)Proof-of-Work (Mining) 与 权威证明(联合签名)Proof-of-Authority (Federated Signing)
https://github.com/ethereum/guide/blob/master/poa.md (opens new window)
# Morden(原始测试网)
https://blog.ethereum.org/2016/11/20/from-morden-to-ropsten/ (opens new window)
# Ropsten
如果你想开始在Ropsten网络上测试合约,有几个faucet可以供给你Ropsten的ether。如果faucet不起作用,请尝试不同 的faucet。
http://faucet.ropsten.be:3001/ (opens new window) 这个faucet提供了应该排队接收测试以太的地址的可能性。
bitfwd Ropsten Faucet https://faucet.bitfwd.xyz/ (opens new window)。
Kyber Network Ropsten Faucet https://faucet.kyber.network/ (opens new window)。
MetaMask Ropsten Faucet https://faucet.metamask.io/ (opens new window)
Ropsten Testnet Mining Pool http://pool.ropsten.ethereum.org/ (opens new window)
Etherscan Ropsten Pool https://ropsten.etherscan.io/ (opens new window)
# Rinkeby
Rinkeby水龙头位于https://faucet.rinkeby.io/ (opens new window)。 要请求测试ether,有必要在Twitter,Google Plus或Facebook上发布公开信息。https://www.rinkeby.io/ (opens new window) https://rinkeby.etherscan.io/ (opens new window)
# Kovan
Kovan testnet支持各种方法来请求测试ether。 更多信息可以在 https://github.com/kovan-testnet/faucet/blob/master/README.md (opens new window) 找到。
https://kovan-testnet.github.io/website/ (opens new window)
https://kovan.etherscan.io/ (opens new window)
# 以太坊经典Testnets
# Morden
以太坊经典目前运行着Morden测试网的一个变体,与以太坊经典活跃网络保持功能相同。你可以通过gastracker
RPC或者为geth
或parity
提供一个标志来连接它.
Faucet: http://testnet.epool.io/ (opens new window)
Gastracker RPC: https://web3.gastracker.io/morden (opens new window)
Block Explorer: http://mordenexplorer.ethertrack.io/home (opens new window)
Geth flag: geth --chain=morden
Parity flag: parity --chain#classic-testnet
# 以太坊测试网的历史
Olympic, Morden to Ropsten, Kovan, Rinkeby
Olympic testnet (Network ID: 0) 是Frontier首个公共测试网(简称Ethereum 0.9)。它于2015年初推出,2015年中期 被Morden取代时弃用。
Ethereum’s Morden testnet (Network ID: 2) 与Frontier一起发布,从2015年7月开始运行,直到2016年11月不再使用。 虽然任何使用以太坊的人都可以创建测试网,但Morden是第一个“官方”公共测试网,取代了Olympic测试网。由于臃肿区块链的 长同步时间以及Geth和Parity客户端之间的共识问题,测试网络重新启动并重新生成为Ropsten。
Ropsten (Network ID: 3) 是一个针对Homestead的公共跨客户端测试网,于2016年晚些时候推出,并作为公共测试网顺利 运行至2017年2月底。根据Ethereum的核心开发人员PéterSzilágyi的说法,二月的时候,“恶意行为者决定滥用低PoW,并 逐步将gas限制提高到90亿(从普通的470万),发送巨大交易损害了整个网络”。Ropsten在2017年3月被恢复。 https://github.com/ethereum/ropsten (opens new window)
Kovan (Network ID: 42) 是由Parity的权威证明(PoA)共识算法驱动的Homestead的公共Parity测试网络。该测试网不受 垃圾邮件攻击的影响,因为ether供应由可信方控制。这些值得信赖的各方是在Ethereum上积极开发的公司。 尽管看起来这应该是以太坊测试网问题的解决方案,但在以太坊社区内似乎存在关于Kovan测试网的共识问题。 https://github.com/kovan-testnet/proposal (opens new window)
Rinkeby (Network ID: 4) 是由Ethereum团队于2017年4月开始的Homestead发布的Geth测试网络,并使用PoA共识协议。 以斯德哥尔摩的地铁站命名,它几乎不受垃圾邮件攻击的影响(因为以太网供应由受信任方控制)。请参阅EIP 225: https://github.com/ethereum/EIPs/issues/225 (opens new window)
# 工作量证明(挖矿)Proof-of-Work (Mining) 与 权威证明(联合签名)Proof-of-Authority (Federated Signing)
https://github.com/ethereum/guide/blob/master/poa.md (opens new window)
Proof-of-Work 是一种协议,必须执行挖矿(昂贵的计算机计算)以在区块链(分布式账本)上创建新的区块(去信任的交易)。 缺点:能源消耗。集中的哈希算力与集中的采矿农场,不是真正的分布式。挖掘新块体所需的大量计算能力对环境有影响。
Proof-of-Authority 是一种协议,它只将造币的负载分配给授权和可信的签名者,他们可以根据自己的判断并随时以发币频率 分发新的区块。https://github.com/ethereum/EIPs/issues/225 (opens new window) 优点:具有最显赫的身份的区块链参与者通过算法选择来验证块来交付交易。
# 运行本地测试网
# Ganache: 以太坊开发的个人区块链
你可以使用Ganache部署合约,开发应用程序并运行测试。它可用作Windows,Mac和Linux的桌面应用程序。
网站: http://truffleframework.com/ganache (opens new window)
# Ganache CLI: Ganache 作为命令行工具。
这个工具以前称为“ethereumJS TestRPC”。
https://github.com/trufflesuite/ganache-cli/ (opens new window)
$ npm install -g ganache-cli
让我们开始以太坊区块链协议的节点模拟。
- []检查
--networkId
和--port
标志值是否与truffle.js中的配置相匹配 - []检查
--gasLimit
标志值是否与https://ethstats.net上显示的最新主网gas极限(即8000000 gas)相匹配,以避免 不必要地遇到`gas'异常。请注意,4000000000的“--gasPrice”代表4 gwei的gas价格。 - []可以输入一个`--mnemonic'标志值来恢复以前的高清钱包和相关地址
$ ganache-cli \
--networkId=3 \
--port="8545" \
--verbose \
--gasLimit=8000000 \
--gasPrice=4000000000;
2
3
4
5
6
# 第五章 密钥和地址
以太坊的基础技术之一是 密码学 cryptography,它是数学的一个分支,广泛用于计算机安全。密码学在希腊文中的意思是 “秘密写作”,但密码学的科学不仅仅包含秘密协作,它被称为加密。加密也可以用来证明秘密的知识而不泄露该秘密(数字签名), 或者证明数据的真实性(数字指纹)。这些类型的密码学证明是以太坊和大多数区块链系统的关键数学工具,广泛用于以太坊应用。 讽刺的是,加密并不是以太坊的重要组成部分,因为它的通信和交易数据没有加密,也不需要加密以保护系统。在本章中,我们将 以密钥和地址的形式介绍一些以太坊用来控制资金所有权的密码学。
# 简介
以太坊有两种不同类型的账户,可以拥有和控制ether:外部所有账户(EOA)和_合同_。在本节中,我们将研究使用密码学来确定 外部所有账户(即私人密钥)对ether的所有权。
EOAs中以太的所有权通过 数字密钥 digital keys,以太坊地址_和_数字签名 建立 。数字密钥实际上并不存储在区块链中 或在以太坊网络上传输,而是由用户创建并存储在文件或称为_钱包_的简单数据库中。用户钱包中的数字密钥完全独立于以太坊协议, 可以由用户的钱包软件生成和管理,无需参考区块链或访问互联网。数字密钥可实现以太坊的许多有趣特性,包括去中心化的信任和 控制以及所有权证明。
以太坊交易需要将有效的数字签名包含在区块链中,该签名只能使用密钥生成;因此,任何拥有该密钥副本的人都可以控制ether。以 太坊交易中的数字签名证明了资金的真正所有者。
数字密钥成对组成,密钥和公钥。将公钥视为类似于银行帐号,私钥类似于私密PIN,用于控制帐户。以太坊的用户很少看到这些数字密 钥。在大多数情况下,它们存储在钱包文件内并由以太坊钱包软件管理。
在以太坊交易的付款部分中,预期收款人由_以太坊地址_表示,该地址与支票上的收款人名称相同(即“付款给谁”)。在大多数情况下, 以太坊地址是从公钥生成并对应的。但是,并非所有以太坊地址都代表公钥。他们也可以代表合同,我们将在 [contracts] 中看到。 以太坊地址是用户常会看到的唯一密钥表示,因为这是他们需要与世界分享的部分。
首先,我们将介绍密码学并解释以太坊使用的数学。接下来,我们将看看密钥是如何生成,存储和管理的。最后,我们将回顾用于表示私钥 和公钥以及地址的各种编码格式。
# 公钥密码技术和加密货币
公钥密码技术是现代信息安全的核心概念。首先由Martin Hellman,Whitfield Diffie和Ralph Merkle在20世纪70年代公开发明的,这是 一个巨大的突破,它激起了公众对密码学领域的广泛兴趣。在70年代以前,强大的密码学知识在政府的控制下,很少有公开的研究,直到公钥密 码技术研究的公开发表。
公钥密码系统使用唯一的密钥来保护信息。这些独特的密钥基于具有独特属性的数学函数:它们很容易在一个方向上计算,但很难在相反方向上 计算。基于这些数学函数,密码学能够创建数字密钥和不可伪造的数字签名,这些签名由数学定律保证。
例如,计算两个大素数的乘积是微不足道的。但是给定两个大素数的乘积,很难找到这两个素数(称为_素因式分解_问题)。假设我提供数字6895601 并告诉你它是两个素数的乘积。找到这两个素数要比让它们相乘生产6895601要困难得多。
如果你知道一些秘密信息,这些数学函数可以很容易地被反转。在我们上面的例子中,如果我告诉你一个主素数是1931,你可以简单地用一个简单的 除法找到另一个:6895601/1931 = 3571。这样的函数被称为_trapdoor函数_因为给定一个秘密信息,你可以采取一个快捷方式,使得反转该函数很简单。
在密码学中有用的另一类数学函数基于椭圆曲线上的算术运算。在椭圆曲线算术中,乘以模数是简单的,但是除法是不可能的(一个被称为_离散对数_的问题) 。椭圆曲线密码术在现代计算机系统中被广泛使用,并且是以太坊(和其他加密货币)数字密钥和数字签名的基础。
Tip
更多关于密码学和现代密码学中使用的数学函数:
密码: https://en.wikipedia.org/wiki/Cryptography (opens new window)
Trapdoor函数: https://en.wikipedia.org/wiki/Trapdoor_function (opens new window)
素因子分解: https://en.wikipedia.org/wiki/Integer_factorization (opens new window)
离散对数: https://en.wikipedia.org/wiki/Discrete_logarithm (opens new window)
椭圆曲线密码学: https://en.wikipedia.org/wiki/Elliptic_curve_cryptography
在以太坊,我们使用公钥加密技术来创建一个密钥对,以控制对ether的访问,并允许我们对合同进行身份验证。密钥对由私钥和唯一公钥组成,并且被认为是“一对儿”, 因为公钥是从私钥中派生出来的。公钥用于接收资金,私钥用于创建数字签名来签署交易以支付资金。数字签名也可用于验证合同的所有者或用户,我们将在 [contract_authentication] 中看到。
公钥和私钥之间存在数学关系,允许私钥用于在消息上生成签名。该签名可以在不公开私钥的情况下使用公钥进行验证。
当使用ether时,当前所有者在交易中呈现她的公钥和签名(每次不同,但是使用相同的私钥创建)。通过公钥和签名,以太坊系统中的每个人都可以独立验证并接受交易 的有效性,从而确认在转移ether的人拥有他们。
TIP
在大多数钱包实现中,为了方便起见,私钥和公钥一起存储为_key pair_。但是,公钥可以由私钥进行简单计算,因此只存储私钥也是可以的。
为什么使用不对称加密(公钥/私钥)?
为什么在以太坊使用非对称密码术?它不习惯“加密”(保密)交易。相反,非对称密码术的有用特性是产生数字签名的能力。私钥可应用产生交易的数字签名。 这个签名只能由知道私钥的人制作。但是,任何有权访问公钥和交易签名的人都可以使用它们来验证。非对称加密技术的这一有用特性使任何人都可以验证每 笔交易的每个签名,
# 私钥
私钥只是一个随机选取的数字。私有密钥的所有权和控制权是用户控制与相应以太坊地址相关联的所有资金的基础,也是对该地址的合同的访问权授权。通过 证明交易中使用的资金的所有权,私钥用于创建花费ether所需的签名。私钥在任何时候都必须保密,因为向第三方透露密钥相当于让他们控制以太和由该密 钥保证的合同。私钥还必须备份并防止意外丢失。如果它丢失了,无法恢复,它保护的资金也将永远丢失。
TIP
以太坊私钥只是一个数字。你可以使用硬币,铅笔和纸随机挑选你的私钥:投掷硬币256次,得到可以在以太坊钱包中使用的随机二进制数字作为私钥。 然后可以从私钥生成公钥和地址。
# 从随机数生成私钥
生成密钥的第一步也是最重要的一步是找到一个安全的熵源或随机源。创建以太坊私钥基本上与“选择1到2^256^之间的数字”相同。只要不可预测和不可重复, 用于选择该数字的确切方法并不重要。以太坊软件使用底层操作系统的随机数生成器生成256位熵(随机性)。通常,操作系统随机数生成器是由一个人为的 随机源进行初始化的,这就是为什么可能会要求你将鼠标左右摇摆几秒钟,或者按下键盘上的随机键。
更确切地说,可能的私钥范围略小于 。在以太坊中,私钥可以是1
和n-1
之间的任何数字,其中n是定义为使用的椭圆曲线的阶数的常数
( ,略小于 )(参见[elliptic_curve])。为了创建这样的密钥,我们随机选择一个256位数字并检查它是否小于n-1
。
在编程方面,这通常是通过将从密码学安全的随机源收集的更大的随机比特串提供给256位哈希算法(如Keccak-256或SHA256)
(参见 [cryptographic_hash_algorithm]),产生一个256位数字。如果结果小于n-1
,我们有一个合适的私钥。否则,我们只需再次尝试使用另一个随机数。
WARNING
不要编写自己的代码来创建随机数或使用你的编程语言提供的“简单”随机数发生器。使用密码学安全的伪随机数字发生器(CSPRNG)和来自足够熵源的种子。研究 你选择的随机数生成器库的文档,以确保其是密码学安全的。正确实施CSPRNG对于密钥的安全至关重要。
以下是以十六进制格式显示的随机生成的私钥(k)(256位,显示为64个十六进制数字,每个4位):
f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315
TIP
以太坊的私人密钥空间的大小( )是一个难以置信的大数目。十进制大约是 。可见宇宙估计含有 原子。
# 公钥
以太坊公钥是一个椭圆曲线上的_点_ point,意思是它是一组满足椭圆曲线方程的X和Y坐标。
简单来说,以太坊公钥是两个数字,并联在一起。这些数字是通过一次单向的计算从私钥生成的。这意味着,如果你拥有私钥,则计算公钥是微不足道的。 但是你不能从公钥中计算私钥。
WARNING
MATH即将发生!不要惊慌。如果你发现难以阅读前一段,则可以跳过接下来的几节。有很多工具和库会为你做数学。
公钥使用椭圆曲线乘法和私钥计算,这是不可逆的:K = k * G,其中_k_是私钥,_G_是一个称为_generator point_的常数点, K_是结果公钥。如果你知道_K,那么称为“寻找离散对数”的逆运算就像尝试所有可能的_k_值一样困难,也就是蛮力搜索。
简单地说:椭圆曲线上的算术不同于“常规”整数算术。点(G)可以乘以整数(k)以产生另一点(K)。但是没有_除法_这样的东西,所以不可能简单地用公共 密钥K除以点G来计算私钥k。这是[pkc]中描述的单向数学函数。
TIP
椭圆曲线乘法是密码学家称之为“单向”函数的一种函数:在一个方向(乘法)很容易完成,而在相反方向(除法)不可能完成。私钥的所有者可以很容易地创建公钥, 然后与世界共享,因为知道没有人能够反转该函数并从公钥计算私钥。这种数学技巧成为证明以太坊资金所有权和合同控制权的不可伪造和安全数字签名的基础。
在我们演示如何从私钥生成公钥之前,我们先来看一下椭圆曲线加密。
# 椭圆曲线密码学解释
椭圆曲线密码术是一种基于离散对数问题的非对称或公钥密码体系,如椭圆曲线上的加法和乘法运算。
[ecc-curve] 是椭圆曲线的一个例子,类似于以太坊使用的曲线。
TIP
以太坊使用与比特币完全相同的椭圆曲线,称为 secp256k1
。这使得重新使用比特币的许多椭圆曲线库和工具成为可能。

Figure 1. A visualization of an elliptic curve
以太坊使用特定的椭圆曲线和一组数学常数,由国家标准与技术研究院(NIST)制定的名为
secp256k1
的标准中所定义的。
secp256k1
曲线由以下函数定义,该函数产生一个椭圆曲线: 或
mod p (模素数p) 表示该曲线在素数阶_p_的有限域上,也写作 latexmath:[( \mathbb{F}_p )], 其中 , 一个非常大的素数。
因为这条曲线是在有限的素数阶上而不是在实数上定义的,所以它看起来像是一个散布在二维中的点的模式,使得难以可视化。
然而,数学与实数上的椭圆曲线的数学是相同的。作为一个例子,[ecc-over-F17-math] 在一个更小的素数阶17的有限域
上显示了相同的椭圆曲线,显示了一个网格上的点的图案。secp256k1
以太坊椭圆曲线可以被认为是一个更复杂的模式,在一个不可思议的大网格上的点。
Figure 2. Elliptic curve cryptography: visualizing an elliptic curve over F(p), with p=17
例如,以下是坐标为(x,y)的点Q,它是
secp256k1
曲线上的一个点: Q = (49790390825249384486033144355916864607616083520101638681403973749255924539515, 59574132161899900045862086493921015780032175291755807399284007721050341297360)
[Using Python to confirm that this point is on the elliptic curve ] 显示了如何使用Python检查它。变量x和y是上述点Q的坐标。变量p是椭圆曲线的主要
阶数(用于所有模运算的素数)。Python的最后一行是椭圆曲线方程(Python中的%运算符是模运算符)。如果x和y确实是椭圆曲线上的点,那么它们满足方程,
结果为零(0L
是零值的长整数)。通过在命令行上键入python
并复制下面的每行(不包括提示符 >>>
),亲自尝试一下:
Example 1. Using Python to confirm that this point is on the elliptic curve
Python 3.4.0 (default, Mar 30 2014, 19:23:13)
[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.38)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> p = 115792089237316195423570985008687907853269984665640564039457584007908834671663
>>> x = 49790390825249384486033144355916864607616083520101638681403973749255924539515
>>> y = 59574132161899900045862086493921015780032175291755807399284007721050341297360
>>> (x ** 3 + 7 - y**2) % p
0L
2
3
4
5
6
7
8
# 椭圆曲线算术运算
很多椭圆曲线数学看起来很像我们在学校学到的整数算术。具体而言,我们可以定义一个加法运算符,而不是添加数字就是在曲线上添加点。一旦我们有了加法运算符, 我们也可以定义一个点和一个整数的乘法,等于重复加法。
A lot of elliptic curve math looks and works very much like the integer arithmetic we learned at school. Specifically, we can define an addition operator, which instead of adding numbers is adding points on the curve. Once we have the addition operator, we can also define multiplication of a point and a whole number, such that it is equivalent to repeated addition.
加法定义为给定椭圆曲线上的两个点 and , 第三个点 , 也在椭圆曲线上。
在几何上,这个第三点 是通过在 和 之间画一条直线来计算的。这条线将在另外一个地方与椭圆曲线相交。称此点为 P_3^' = (x, y)。 然后在x轴上反射得到 。
在椭圆曲线数学中,有一个叫做“无穷点”的点,它大致对应于零点的作用。在计算机上,它有时用 x = y = 0表示(它不满足椭圆曲线方程,但它是一个容易区分的情况, 可以检查)。有几个特殊情况解释了“无穷点”的需要。
如果 和 是同一点, and 之间的直线应该延伸到曲线上 P~1~ 的切线。 该切线恰好与曲线在一个新点相交。 你可以使用微积分技术来确定切线的斜率。我们将我们的兴趣局限在具有两个整数坐标的曲线上,这些技巧令人好奇地工作!
在某些情况下(即,如果 和 具有相同的x值但不同的y值),切线将精确地垂直,在这种情况下 =“无穷点”。
如果 是“无穷点”,那么 。 类似地, 如果 是“无穷点”,。这显示了无穷点如何扮演零在“正常” 算术中扮演的角色。
是可结合的, $ (A + B) + C = A + (B + C)$ . 这表示 $A + B + C $ 不加括号也没有歧义。
现在我们已经定义了加法,我们可以用扩展加法的标准方式来定义乘法。对于椭圆曲线上的点P,如果k是整数, 则 。请注意,在这种情况下,k有时会被混淆地称为“指数”。
# 生成一个公钥
以一个随机生成的数字_k_的私钥开始,我们通过将它乘以称为_generator point_ G_的曲线上的预定点,在曲线上的其他位置产生另一个点,这是相应的公钥_K。
生成点被指定为secp256k1
标准的一部分,对于secp256k1
的所有实现始终相同,并且从该曲线派生的所有密钥都使用相同的点_G_:
其中_k_是私钥,_G_是生成点,K_是生成的公钥,即曲线上的一个点。因为所有以太坊用户的生成点始终相同,所以_G_乘以_G_的私钥总是会导致相同的公钥_K。k_和_K_之间的关系是固定的,但只能从_k_到_K_的一个方向进行计算。这就是为什么以太坊地址(来自_K) 可以与任何人共享,并且不会泄露用户的私钥(k)。
正如我们在 [ 椭圆曲线算术运算] 中所描述的那样,k * G的乘法相当于重复加,G + G + G + ... + G ,重复k次。总而言之,为了从私钥k
生成公钥K
,
我们将生成点G
添加到自己k
次。
TIP
私钥可以转换为公钥,但公钥不能转换回私钥,因为数学只能单向工作。
让我们应用这个计算来找到我们在 [私钥] 中给出的特定私钥的公钥:
Example private key to public key calculation
K = f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315 * G
密码库可以帮助我们使用椭圆曲线乘法计算K值。得到的公钥K
被定义为一个点 K = (x,y)
:
Example public key calculated from the example private key
K = (x, y)
where,
x = 6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b
y = 83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0
2
3
4
5
6
在以太坊中,你可以看到公钥以66个十六进制字符(33字节)的十六进制序列表示。这是从行业联盟标准高效密码组(SECG)提出的标准序列化格式采用的, 在http://www.secg.org/sec1-v2.pdf (opens new window)[Standards for Efficient Cryptography(SEC1)]中有记载。 该标准定义了四个可用于识别椭圆曲线上点的可能前缀:
Prefix | Meaning | Length (bytes counting prefix) |
0x00 | Point at infinity | 1 |
0x04 | Uncompressed point | 65 |
0x02 | Compressed point with even y | 33 |
0x03 | Compressed point with odd y | 33 |
以太坊只使用未压缩的公钥,因此唯一相关的前缀是(十六进制)04
。顺序连接公钥的X和Y坐标:
04 ` X-coordinate (32 bytes/64 hex) ` Y coordinate (32 bytes/64 hex)
因此,我们在 [ Example public key calculated from the example private key ] 中计算的公钥被序列化为:
046e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0
# 椭圆曲线库
加密货币相关项目中使用了secp256k1椭圆曲线的几个实现:
OpenSSL
OpenSSL库提供了一套全面的加密原语,包括secp256k1的完整实现。例如,要派生公钥,可以使用函数EC_POINT_mul()
。https://www.openssl.org/ (opens new window)
libsecp256k1
Bitcoin Core的libsecp256k1是secp256k1椭圆曲线和其他密码原语的C语言实现。椭圆曲线密码学的libsecp256是从头开始编写的,代替了Bitcoin Core
软件中的OpenSSL,在性能和安全性方面被认为是优越的。https://github.com/bitcoin-core/secp256k1 (opens new window)
# 加密哈希函数
加密哈希函数在整个以太坊使用。事实上,哈希函数几乎在所有密码系统中都有广泛应用,这是密码学家布鲁斯•施奈尔(Bruce Schneier)所说的一个事实, 他说:“单向哈希函数远不止于加密算法,而是现代密码学的主要工具。
在本节中,我们将讨论哈希函数,了解它们的基本属性以及这些属性如何使它们在现代密码学的很多领域如此有用。我们在这里讨论哈希函数,因为它们是将以太坊 公钥转换成地址的一部分。
简而言之,“哈希函数是可用于将任意大小的数据映射到固定大小的数据的函数。” https://en.wikipedia.org/wiki/Hash_function[Source:Wikipedia]。 哈希函数的输入称为 原象 _ pre-image_ 或 消息 message。输出被称为 哈希 _hash_或 摘要 digest。哈希函数的一个特殊子类别是 加密哈希函数, 它具有对密码学有用的特定属性。
加密哈希函数是一种_单向_哈希函数,它将任意大小的数据映射到固定大小的位串,如果知道输出,计算上不可能重新创建输入。确定输入的唯一方法是对所有可能的输入 进行蛮力搜索,检查匹配输出。
加密哈希函数有五个主要属性 (https://en.wikipedia.org/wiki/Cryptographic_hash_function[Source: Wikipedia/Cryptographic Hash Function]):
确定性
任何输入消息总是产生相同的哈希摘要。
可验证性
计算消息的哈希是有效的(线性性能)。
不相关
对消息的小改动(例如,一位改变)会大幅改变哈希输出,以致它不能与原始消息的哈希相关联。
不可逆性
从哈希计算消息是不可行的,相当于通过可能的消息进行蛮力搜索。
碰撞保护
计算两个不同的消息产生相同的哈希输出应该是不可行的。
碰撞保护对于防止以太坊中的数字签名伪造至关重要。
这些属性的组合使加密哈希函数可用于广泛的安全应用程序,包括:
- 数据指纹识别
- 消息完整性(错误检测)
- 工作证明
- 认证(密码哈希和密钥扩展)
- 伪随机数发生器
- 原象承诺
- 唯一标识符
通过研究系统的各个层面,我们会在以太坊找到它的很多应用。
# 以太坊的加密哈希函数 - Keccak-256
以太坊在许多地方使用_Keccak-256_加密哈希函数。Keccak-256被设计为于2007年举行的SHA-3密码哈希函数竞赛的候选者。Keccak是获胜的算法, 在2015年被标准化为 FIPS(联邦信息处理标准)202。
然而,在以太坊开发期间,NIST标准化工作正在完成。在标准过程完成后,NIST调整了Keccak的一些参数,据称可以提高效率。这与英雄告密者 爱德华斯诺登透露的文件暗示NIST可能受到国家安全局的不当影响同时发生,故意削弱Dual_EC_DRBG随机数生成器标准,有效地在标准随机数生成器 中放置一个后门。这场争论的结果是对所提议修改的反对以及SHA-3标准化的严重拖延。当时,以太坊基金会决定实施最初的Keccak算法。
WARNING
虽然你可能在Ethereum文档和代码中看到“SHA3”,但很多(如果不是全部)这些实例实际上是指Keccak-256,而不是最终确定的FIPS-202 SHA-3标准。 实现差异很小,与填充参数有关,但它们的重要性在于Keccak-256在给定相同输入的情况下产生与FIPS-202 SHA-3不同的哈希输出。
由于Ethereum中使用的哈希函数(Keccak-256)与最终标准(FIP-202 SHA-3)之间的差异造成了混淆,因此正在努力将代码中所有的 sha3
的所有实例,
操作码和库重新命名为 keccak256
。详情请参阅https://github.com/ethereum/EIPs/issues/59[ERC-59] (opens new window)。
# 我正在使用哪个哈希函数?
如何判断你使用的软件库是FIPS-202 SHA-3还是Keccak-256(如果两者都可能被称为“SHA3”)?
一个简单的方法是使用_test vector_,一个给定输入的预期输出。最常用于哈希函数的测试是_empty input_。如果你使用空字符串作为输入运行哈希函数, 你应该看到以下结果:
Testing whether the SHA3 library you are using is Keccak-256 of FIP-202 SHA-3
Keccak256("") =
c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
SHA3("") =
a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a
2
3
4
5
因此,无论调用什么函数,都可以通过运行上面的简单测试来测试它是否是原始的Keccak-256或最终的NIST标准FIPS-202 SHA-3。请记住, 以太坊使用Keccak-256,尽管它在代码中通常被称为SHA-3。
接下来,让我们来看一下Ethereum中Keccak-256的第一个应用,即从公钥生成以太坊地址。
# 以太坊地址
以太坊地址是 唯一标识符 unique identifiers,它们是使用单向哈希函数(Keccak-256)从公钥或合约派生的。
在我们之前的例子中,我们从一个私钥开始,并使用椭圆曲线乘法来派生一个公钥:
Private Key k
:
k = f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315
Public Key K
(X and Y coordinates concatenated and shown as hex):
K = 6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0
WARNING
值得注意的是,在计算地址时,公钥没有用前缀(十六进制)04格式化。
我们使用Keccak-256来计算这个公钥的_hash_:
Keccak256(K) = 2a5bc342ed616b5ba5732269001d3f1ef827552ae1114027bd3ecf1f086ba0f9
然后我们只保留最后的20个字节(大端序中的最低有效字节),这是我们的以太坊地址:
001d3f1ef827552ae1114027bd3ecf1f086ba0f9
大多数情况下,你会看到带有前缀“0x”的以太坊地址,表明它是十六进制编码,如下所示:
0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9
# 以太坊地址格式
以太坊地址是十六进制数字,从公钥的Keccak-256哈希的最后20个字节导出的标识符。
与在所有客户端的用户界面中编码的比特币地址不同,它们包含内置校验和来防止输入错误的地址,以太坊地址以原始十六进制形式呈现,没有任何校验和。
该决定背后的基本原理是,以太坊地址最终会隐藏在系统高层的抽象(如名称服务)之后,并且必要时应在较高层添加校验和。
回想起来,这种设计选择导致了一些问题,包括由于输入错误地址和输入验证错误而导致的资金损失。以太坊名称服务的开发速度低于最初的预期, 诸如ICAP之类的替代编码被钱包开发商采用得非常缓慢。
# 互换客户端地址协议 Inter Exchange Client Address Protocol (ICAP)
互换客户端地址协议(ICAP)_是一种部分与国际银行帐号(IBAN)编码兼容的以太坊地址编码,为以太坊地址提供多功能,校验和互操作编码。ICAP地址可以编码 以太坊地址或通过以太坊名称注册表注册的常用名称。
阅读以太坊Wiki上的ICAP:https://github.com/ethereum/wiki/wiki/ICAP:-Inter-exchange-Client-Address-Protocol (opens new window)
IBAN是识别银行账号的国际标准,主要用于电汇。它在欧洲单一欧元支付区(SEPA)及其以后被广泛采用。IBAN是一项集中和严格监管的服务。 ICAP是以太坊地址的分散但兼容的实现。
一个IBAN由含国家代码,校验和和银行账户标识符(特定国家)的34个字母数字字符(不区分大小写)组成。
ICAP使用相同的结构,通过引入代表“Ethereum”的非标准国家代码“XE”,后面跟着两个字符的校验和以及3个可能的账户标识符变体:
Direct
最多30个字母数字字符big-endian base-36整数,表示以太坊地址的最低有效位。由于此编码适合小于155位,因此它仅适用于以一个或多个零字节开头的
以太坊地址。就字段长度和校验和而言,它的优点是它与IBAN兼容。示例:XE60HAMICDXSV5QXVJA7TJW47Q9CHWKJD
(33个字符长)
Baasic
与“Direct”编码相同,只是长度为31个字符。这使它可以编码任何以太坊地址,但使其与IBAN字段验证不兼容。
示例:XE18CHDJBPLTBCJ03FE9O2NS0BPOJVQCU2P
(35个字符长)
Indrect
编码通过名称注册表提供程序解析为以太坊地址的标识符。使用由_asset identifier_(例如ETH),名称服务(例如XREG)和9个字符的名称
(例如KITTYCATS)组成的16个字母数字字符,这是一个人类可读的名称。示例:XEpass:[##] ETHXREGKITTYCATS(
20个字符长),
其中“##”应由两个计算校验和字符替换。
我们可以使用 helpeth 命令行工具来创建ICAP地址。让我们尝试使用我们的示例私钥(前缀为0x并作为参数传递给helpeth):
$ helpeth keyDetails -p 0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315
Address: 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9
ICAP: XE60 HAMI CDXS V5QX VJA7 TJW4 7Q9C HWKJ D
Public key: 0x6e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0
2
3
4
5
helpeth
命令为我们构建了一个十六进制以太坊地址以及一个ICAP地址。我们示例密钥的ICAP地址是:
XE60HAMICDXSV5QXVJA7TJW47Q9CHWKJD
由于我们的示例以太坊地址恰好以零字节开始,因此可以使用IBAN格式中有效的“Direct”ICAP编码方法进行编码。因为它是33个字符长。
如果我们的地址不是从零开始,那么它将被编码为“Basic”编码,这将是35个字符长并且作为IBAN格式无效。
TIP
以零字节开始的任何以太坊地址的概率是1/256。为了生成这样一个类型,在我们找到一个作为IBAN兼容的“Direct”编码之前, 它将平均用256个不同的随机私钥进行256次尝试ICAP地址。
不幸的是,现在,只有几个钱包支持ICAP。
# 使用大写校验和的十六进制编码 (EIP-55)
由于ICAP或名称服务部署缓慢,因此提出了一个新的标准,以太坊改进建议55(EIP-55)。你可以阅读详细信息:
https://github.com/Ethereum/EIPs/blob/master/EIPS/eip-55.md (opens new window)
通过修改十六进制地址的大小写,EIP-55为以太坊地址提供了向后兼容的校验和。这个想法是,以太坊地址不区分大小写,所有钱包都应该接受以大写字母或 小写字母表示的以太坊地址,在解释上没有任何区别。
通过修改地址中字母字符的大小写,我们可以传达一个校验和,可以用来保护地址完整性,防止输入或读取错误。不支持EIP-55校验和的钱包简单地忽略地址 包含混合大写的事实。但那些支持它的人可以验证它并以99.986%的准确度检测错误。
混合大小写编码很微妙,最初你可能不会注意到它。我们的示例地址是:
0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9
使用 EIP-55 混合大小写校验和,它变为:
0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9
你能看出区别吗?一些来自十六进制编码字母表的字母(AF)字符现在是大写字母,而另一些则是小写字母。除非你仔细观察,否则你甚至可能没有注意到其中的差异。
EIP-55实施起来相当简单。我们采用小写十六进制地址的Keccak-256哈希。这个哈希作为地址的数字指纹,给我们一个方便的校验和。输入(地址)中的任何小改动都 会导致哈希结果(校验和)发生很大变化,从而使我们能够有效地检测错误。然后我们的地址的哈希被编码为地址本身的大写字母。让我们一步步分解它:
- 计算小写地址的哈希,不带
0x
前缀::
Keccak256("001d3f1ef827552ae1114027bd3ecf1f086ba0f9")
23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9695d9a19d8f673ca991deae1
2
- 如果哈希的相应十六进制数字大于或等于
0x8
,则将每个字母地址字符大写。如果我们排列地址和哈希,这将更容易显示:
Address: 001d3f1ef827552ae1114027bd3ecf1f086ba0f9
Hash : 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9...
2
我们的地址在第四个位置包含一个字母 d
。哈希的第四个字符是 6
,小于 8
。所以,我们保持 d
小写。我们地址中的下一个字母字符是 f
,
位于第六位。十六进制哈希的第六个字符是 c
,它大于 8
。因此,我们在地址中大写 F
,等等。正如你所看到的,我们只使用哈希的前20个字节
(40个十六进制字符)作为校验和,因为我们只有20个字节(40个十六进制字符)能正确地大写。
检查自己产生的混合大写地址,看看你是否可以知道在地址哈希中哪些字符被大写和它们对应的字符:
Address: 001d3F1ef827552Ae1114027BD3ECF1f086bA0F9
Hash : 23a69c1653e4ebbb619b0b2cb8a9bad49892a8b9...
2
# 在EIP-55编码地址中检测错误
现在,我们来看看EIP-55地址如何帮助我们发现错误。假设我们已经打印出ETHER-E编码的以太坊地址:
0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9
现在,让我们在阅读该地址时犯一个基本错误。最后一个字符之前的字符是大写字母“F”。对于这个例子,我们假设我们误解为大写“E”。 我们在钱包中输入(不正确的地址):
0x001d3F1ef827552Ae1114027BD3ECF1f086bA0E9
幸运的是,我们的钱包符合EIP-55标准!它注意到混合大写字母并试图验证地址。它将其转换为小写,并计算校验和哈希值:
Keccak256("001d3f1ef827552ae1114027bd3ecf1f086ba0e9")
5429b5d9460122fb4b11af9cb88b7bb76d8928862e0a57d46dd18dd8e08a6927
2
如你所见,即使地址只改变了一个字符(事实上,“e”和“f”只相隔1位),地址的哈希值已经根本改变了。这是哈希函数的特性,使它们对校验和非常有用!
现在,让我们排列这两个并检查大小写:
001d3F1ef827552Ae1114027BD3ECF1f086bA0E9
5429b5d9460122fb4b11af9cb88b7bb76d892886...
2
这都是错的!几个字母字符不正确地大写。请记住,大写是_正确的_校验和的编码。
我们输入的地址的大小写与刚刚计算的校验和不匹配,这意味着地址中的内容发生了变化,并且引入了错误。
# 第六章 钱包
在以太坊中,“钱包”一词有几个不同的含义。
在较高层次上,钱包是作为主要用户界面的应用程序。钱包控制对用户资金的访问,管理密钥和地址,追踪余额以及创建和签署交易。
另外,一些以太坊钱包还可以与合约(如代币)进行交互。
狭义上讲,从程序员的角度来看,“钱包”一词是指用于存储和管理用户密钥的系统。每个“钱包”都有一个密钥管理组件。对于一些钱包来说, 这就是全部。其他一些钱包是更广泛类别的一部分,即“浏览器”,它是以太坊去中心化应用或“DApps”的接口。在“钱包”这个术语下混合的 各种类别之间没有明确的区别。
在本节中,我们将把钱包看作私钥的容器,并将其视为用于管理密钥的系统。
# 钱包技术概览
在本节中,我们总结了用于构建用户友好,安全,和灵活的以太坊钱包的技术。
关于以太坊的一个常见误解是以太坊钱包包含ether或代币。实际上,钱包只包含密钥。ether或其他代币记录在以太坊区块链中。用户通过使用钱包中的密钥 签署交易来控制网络上的代币。从某种意义上说,以太坊钱包是一个 钥匙串 keychain。
TIP
以太坊钱包包含密钥,而不是ether或令牌。每个用户都有一个包含密钥的钱包。钱包真的是包含私钥/公钥的钥匙串(参见[private_public_keys])。 用户使用密钥签署交易,从而证明他们拥有ether。ether储存在区块链上。
有两种主要类型的钱包,通过它们包含的密钥是否彼此相关来区分。
第一种类型是 非确定性钱包 nondeterministic wallet,其中每个密钥都是从随机数中独立生成的。密钥不相互关联。这种类型的钱包也被称为 “Just a Bunch Of Keys”,JBOK钱包。
第二种类型的钱包是 确定性钱包 deterministic wallet,其中所有密钥都来自单个主密钥,称为_种子_ seed。这种类型的钱包中的所有钥匙 都是相互关联的,如果有原始种子,可以再次生成。确定性钱包中使用了许多不同的 _密钥推导方法。最常用的派生方法使用树状结构,称为 分层确定 _hierarchical deterministic_或_HD_钱包。
确定性钱包是从种子初始化的。为了使这些更容易使用,种子被编码为一些英文单词(或其他语言的词),称为 mnemonic code 助记词,接下来的几节 将从较高的层次介绍这些技术。
# 非确定性(随机)钱包
在第一个以太坊钱包(由Ethereum pre-sale创建)中,钱包文件存储一个随机生成的私钥。这些钱包正在被确定性的钱包取代,因为它们管理,备份和导入很麻烦。 随机密钥的缺点是,如果你生成了许多密钥,你必须保留所有密钥的副本。每个密钥都必须备份,否则如果钱包变得不可访问,则其控制的资金将不可撤销地丢失。 此外,以太坊地址重用可以通过将多个交易和地址相互关联来降低隐私。0型非确定性钱包是很少的选择,特别是如果你想避免地址重用,因为它意味着管理许多密钥, 需要经常备份。
许多以太坊客户端(包括 go-ethereum 或 geth)使用_keystore_文件,这是一个JSON编码的文件,其中包含一个(随机生成的)私钥,由一个密码加密以 提高安全性。JSON文件的内容如下所示:
{
"address": "001d3f1ef827552ae1114027bd3ecf1f086ba0f9",
"crypto": {
"cipher": "aes-128-ctr",
"ciphertext": "233a9f4d236ed0c13394b504b6da5df02587c8bf1ad8946f6f2b58f055507ece",
"cipherparams": {
"iv": "d10c6ec5bae81b6cb9144de81037fa15"
},
"kdf": "scrypt",
"kdfparams": {
"dklen": 32,
"n": 262144,
"p": 1,
"r": 8,
"salt": "99d37a47c7c9429c66976f643f386a61b78b97f3246adca89abe4245d2788407"
},
"mac": "594c8df1c8ee0ded8255a50caf07e8c12061fd859f4b7c76ab704b17c957e842"
},
"id": "4fcb2ba4-ccdb-424f-89d5-26cce304bf9c",
"version": 3
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
keystore格式使用_Key派生函数(KDF),也称为密码扩展算法,该算法可防止对密码加密的暴力破解,字典或彩虹表攻击。简而言之,私钥没有直接由密码短语加密。相反,通过反复对它进行哈希,密码被_拉长。
哈希函数重复执行262144轮,可以在keystore JSON中的参数 crypto.kdfparams.n
看到。试图暴力破解密码短语的攻击者必须对每个尝试的密码应用262144轮哈希,
这足以减缓攻击行为,从而使破解足够复杂性和够长的密码短语是不可行的。
有许多软件库可以读写keystore格式,例如JavaScript库 keythereum
:
https://github.com/ethereumjs/keythereum (opens new window)
TIP
除简单测试以外,不鼓励使用非确定性钱包,他们太麻烦了,无法备份和使用。相反,使用具有_mnemonic_种子的基于行业标准的_HD钱包_。
# 确定性(种子)钱包
确定性或“种子”钱包是包含私钥的钱包,所有私钥都来源于共同的种子,使用单向哈希函数生成。种子是随机生成的数字,可以与其他数据 (如索引编号或“链码”(请参阅[HD 钱包 (BIP-32/BIP-44)]))组合以导出私钥。在确定性钱包中,种子足以恢复所有派生的密钥, 因此在创建时的单个备份就足够了。种子也足以用于钱包的导入和导出,允许在不同实现的钱包之间轻松迁移所有用户密钥。
# HD 钱包 (BIP-32/BIP-44)
确定性钱包的开发使得从单个”种子“中获得许多密钥变得容易。确定性钱包的最先进的形式是由比特币的BIP-32标准定义的HD钱包。HD钱包包含以树状结构导出的密钥, 以便父密钥可以生成一系列的子密钥,每个子密钥可以派生一系列孙子密钥等等,可以达到无限深度。这个树状结构在 [hd_wallet] 中进行说明。

Figure 1. HD wallet: a tree of keys generated from a single seed
与随机(非确定性)密钥相比,HD钱包具有两大优势。首先,树状结构可以用来表达额外的组织含义,例如,使用特定分支的子密钥来接收传入的支付, 使用不同分支的子秘钥来接收支付时产生的零钱。密钥的分支也可用于公司设置,将不同分支分配给部门,子公司,特定职能或会计类别。
HD钱包的第二个优点是用户可以创建一系列公钥而无需访问相应的私钥。这允许HD钱包用于不安全的服务器上,或者仅用于只查看或只接收的地方, 其中钱包没有可以花费资金的私钥。
# 种子和助记词(BIP-39)
HD钱包是管理许多密钥和地址的非常强大的机制。如果将它们与一系列英文单词(或另一种语言的单词)相结合,更易于转录,和跨钱包的导出导入。 这被称为_mnemonic_,标准由BIP-39定义。今天,许多以太坊钱包(以及用于其他加密货币的钱包)都使用此标准,并且可以使用可互操作的助记词 导入和导出种子以进行备份和恢复。
我们从实际的角度来看一下。下列哪种种子更容易转录,在纸上记录,无误地读取,导出并导入另一个钱包?
A seed for a deterministic wallet, in hex
FCCF1AB3329FD5DA3DA9577511F8F137
A seed for a deterministic wallet, from a 12-word mnemonic
wolf juice proud gown wool unfair
wall cliff insect more detail hub
2
# 钱包最佳实践
随着加密货币钱包技术的成熟,某些常见行业标准使钱包广泛地互操作,易于使用,安全和灵活。这些标准还允许钱包从多个不同的加密货币中获取密钥, 所有这些都来自助记词。这些通用标准是:
- 基于 BIP-39 的助记词
- 基于 BIP-32 的HD钱包
- 基于 BIP-43 的多用途HD钱包
- 基于 BIP-44 的多币种和多账户钱包
这些标准可能会改变,或者可能会因未来的发展而过时,但现在它们形成了一套互联技术,已成为大多数加密货币的事实上的钱包标准。
这些标准已广泛的被软件和硬件钱包采用,使所有这些钱包可以互操作。用户可以导出其中一个钱包上生成的助记词并将其导入另一个钱包,恢复所有交易,密钥和地址。
支持这些标准的软件钱包有 Jaxx,MetaMask,MyEtherWallet(MEW),硬件钱包有:Keepkey,Ledger和Trezor。
以下各节详细介绍了这些技术。
TIP
如果你正在实现以太坊钱包,则应该将其作为HD钱包构建,并将种子编码为易于备份的助记词,并遵循BIP-32,BIP-39,BIP-43和BIP -44标准,如以下各节所述。
# 助记词 (BIP-39)
助记词是表示(编码)派生确定性钱包的种子的随机数的单词序列。单词序列足以重新创建种子,从而重新创建钱包和所有派生的密钥。使用助记词实现的确定性钱包 会在首次创建钱包时向用户展示12至24个字的序列。该单字序列是钱包的备份,可用于在相同或任何兼容的钱包应用程序中恢复和重新创建所有密钥。
TIP
助记词经常与“脑钱包”混淆。他们不一样。主要区别在于脑钱包由用户选择的单词组成,而助记词由钱包随机创建并呈现给用户。这个重要的区别使助记词更加安全,因为人类是非常贫乏的随机性来源。
助记词在BIP-39中定义。请注意,BIP-39是助记词编码标准的一个实现。有一个不同的标准,带有一组不同的单词,在BIP-39之前用于Electrum比特币钱包。 BIP-39由Trezor硬件钱包背后的公司提出,与Electrum的实现不兼容。但是,BIP-39现在已经在数十种可互操作实现方面取得了广泛的行业支持,应该被视 为事实上的行业标准。此外,BIP-39可用于生产支持以太坊的多币种钱包,而Electrum种子不能。
BIP-39定义了助记词和种子的创建,我们在这里通过九个步骤来描述它。为了清楚起见,该过程分为两部分:步骤1至6展示在[generate_mnemonic_words] 中,步骤7至9展示在 [从助记词到种子] 中。
# 生成助记词
助记词是由钱包使用BIP-39中定义的标准化流程自动生成的。钱包从熵源开始,添加校验和,然后将熵映射到单词列表:
- 创建一个128到256位的随机序列(熵)。
- 通过取其SHA256哈希的第一部分(熵长度/32)来创建随机序列的校验和。
- 将校验和添加到随机序列的末尾。
- 将序列按照11bits划分。
- 将每个11bits的值映射到预定义字典中的2048个词中的一个。
- 助记词就是单词的序列。
[generating_entropy_and_encoding] 展示了如何使用熵来生成助记词。

Figure 2. Generating entropy and encoding as mnemonic words
[Mnemonic codes: entropy and word length] 展示熵数据的大小和助记词的长度关系。
Table 1. Mnemonic codes: entropy and word length
Entropy (bits) | Checksum (bits) | Entropy + checksum (bits) | Mnemonic length (words) |
128 | 4 | 132 | 12 |
160 | 5 | 165 | 15 |
192 | 6 | 198 | 18 |
224 | 7 | 231 | 21 |
256 | 8 | 264 | 24 |
# 从助记词到种子
助记符字表示长度为128到256位的熵。然后使用使用密钥扩展函数PBKDF2将熵导出成更长的(512位)种子。然后使用生成的种子构建确定性钱包并派生其密钥。
密钥扩展函数有两个参数:助记词和_salt_。在密钥扩展函数中使用盐的目的是使得构建能够进行暴力攻击的查找表不可行。在BIP-39标准中,盐有另一个 目的 —— 它允许引入密码,作为保护种子的附加安全因素,我们将在 [ BIP-39中的可选密码短语 ] 中详细描述。
步骤7到9中从 [生成助记词] 描述的过程后继续:
- PBKDF2密钥扩展函数的第一个参数是步骤6产生的助记词。
- PBKDF2密钥扩展函数的第二个参数是盐。盐由用户提供的密码字符串和“mnemonic”组合起来。
- PBKDF2使用2048轮HMAC-SHA512哈希算法,扩展助记词和盐,生成512位的种子。
[fig_5_7] 展示如何使用助记词来生成种子。

Figure 3. From mnemonic to seed
TIP
密钥扩展函数及其2048轮哈希对抵御助记词或密码攻击具有一定的有效保护作用。它使(在计算中)尝试超过几千个密码和助记词组合的成本高昂, 因为可能派生的种子数量很大( )。
表格 [#mnemonic_128_no_pass], [#mnemonic_128_w_pass], 和 [#mnemonic_256_no_pass] 展示了一些助记词和它们生成的种子的例子(没有密码)。
Table 2. 128-bit entropy mnemonic code, no passphrase, resulting seed
Entropy input (128 bits) | 0c1e24e5917779d297e14d45f14e1a1a |
Mnemonic (12 words) | army van defense carry jealous true garbage claim echo media make crunch |
Passphrase | (none) |
Seed (512 bits) | 5b56c417303faa3fcba7e57400e120a0ca83ec5a4fc9ffba757fbe63fbd77a89a1a3be4c67196f57c39 a88b76373733891bfaba16ed27a813ceed498804c0570 |
Table 3. 128-bit entropy mnemonic code, with passphrase, resulting seed
Entropy input (128 bits) | 0c1e24e5917779d297e14d45f14e1a1a |
Mnemonic (12 words) | army van defense carry jealous true garbage claim echo media make crunch |
Passphrase | SuperDuperSecret |
Seed (512 bits) | 3b5df16df2157104cfdd22830162a5e170c0161653e3afe6c88defeefb0818c793dbb28ab3ab091897d0 715861dc8a18358f80b79d49acf64142ae57037d1d54 |
Table 4. 256-bit entropy mnemonic code, no passphrase, resulting seed
Entropy input (256 bits) | 2041546864449caff939d32d574753fe684d3c947c3346713dd8423e74abcf8c |
Mnemonic (24 words) | cake apple borrow silk endorse fitness top denial coil riot stay wolf luggage oxygen faint major edit measure invite love trap field dilemma oblige |
Passphrase | (none) |
Seed (512 bits) | 3269bce2674acbd188d4f120072b13b088a0ecf87c6e4cae41657a0bb78f5315b33b3a04356e53d062e5 5f1e0deaa082df8d487381379df848a6ad7e98798404 |
# BIP-39中的可选密码短语
BIP-39标准允许在派生种子时使用可选的密码短语。如果没有使用密码短语,助记词将被一个由常量字符串"mnemonic"
组成的盐扩展,
从任何给定的助记词中产生一个特定的512位种子。如果使用密码短语,则扩展函数会从同一助记词中生成一个_不同的_种子。事实上,
对于一个助记符,每个可能的密码都会生成不同的种子。本质上,没有“错误的”密码。所有密码都是有效的,它们都会生成不同的种子,
形成一大批可能未初始化的钱包。可能的钱包的集合非常大(2^512^),因此没有暴力或意外猜测正在使用的钱包的可能。
TIP
BIP-39中没有“错误”的密码短语。每个密码都会生成一些空钱包,除非以前使用过。
可选的密码短语创造了两个重要的特性:
第二个使得只有助记词没有用的因素(需要记忆的东西),从而保护助记词备份免受小偷的威胁。
一种似是而非的拒绝形式或“胁迫钱包”,一个选定的密码短语会导致一个带有少量资金的钱包,用于将攻击者从包含大部分资金的“真实”钱包吸引开。
但是,重要的是要注意使用密码也会导致丢失的风险。
如果钱包所有者无行为能力或死亡,且其他人不知道密码,则种子无用,钱包中存储的所有资金将永远丢失。
相反,如果所有者在与种子相同的位置备份密码,它会失去第二个因素的目的。
虽然密码短语非常有用,但只能结合精心策划的备份和恢复过程,考虑到主人存活的可能性,并允许其家人恢复加密货币资产。
# 使用助记词
BIP-39 以许多不同的编程语言实现为库:
python-mnemonic (opens new window)
SatoshiLabs团队提出的BIP-39标准的参考实现,使用Python
Consensys/eth-lightwallet (opens new window)
轻量级JS Ethereum节点和浏览器钱包(使用BIP-39)
npm/bip39 (opens new window)
比特币BIP39的JavaScript实现:用于生成确定性密钥的助记词
在独立网页中还有一个BIP-39生成器,对于测试和实验非常有用。[A BIP-39 generator as a standalone web page] 展示了生成助记词, 种子和扩展私钥的独立网页。

Figure 4. A BIP-39 generator as a standalone web page
页面(https://iancoleman.github.io/bip39/ (opens new window))可以在浏览器中离线使用,也可以在线访问。
# 从种子创建HD钱包
HD钱包是由单个_根种子_创建的,该_种子_是128,256或512位随机数。最常见的情况是,这个种子是从_助记词_生成的,详见前一节。
HD钱包中的每个密钥都是从这个根种子确定性地派生出来的,这使得可以在任何兼容的HD钱包中从该种子重新创建整个HD钱包。这使得备份,恢复, 导出和导入包含数千乃至数百万个密钥的HD钱包变得很容易,只需传输根种子的助记词即可。
# 分层确定性钱包(BIP-32)和路径(BIP-43/44)
大多数HD钱包遵循BIP-32标准,这已成为确定性密钥事实上的行业标准代。你可以在以下网址阅读详细说明:
https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki (opens new window)
我们不会在这里讨论BIP-32的细节,只是了解如何在钱包中使用BIP-32。在许多软件库中提供了许多可互操作的BIP-32实现:
Consensys/eth-lightwallet (opens new window)
轻量级JS Ethereum节点和浏览器钱包(使用BIP-32)
还有一个BIP-32独立的网页生成器,对BIP-32的测试和实验非常有用: http://bip32.org/ (opens new window)
Note
独立的BIP-32生成器不是HTTPS网站。提醒你,使用这个工具是不安全的。它仅用于测试。你不应使用本网站制作的密钥(使用实际资金)。
# 扩展公钥和私钥
在BIP-32术语中,可以扩展并产生“孩子”的父密钥称为 扩展密钥 extended key。如果它是一个私有密钥,它是由前缀_xprv_区分的 扩展私钥 extended_private_key:
xprv9s21ZrQH143K2JF8RafpqtKiTbsbaxEeUaMnNHsm5o6wCW3z8ySyH4UxFVSfZ8n7ESu7fgir8imbZKLYVBxFPND1pniTZ81vKfd45EHKX73
扩展公钥 extended public key 由前缀 xpub 区分:
xpub661MyMwAqRbcEnKbXcCqD2GT1di5zQxVqoHPAgHNe8dv5JP8gWmDproS6kFHJnLZd23tWevhdn4urGJ6b264DfTGKr8zjmYDjyDTi9U7iyT
HD钱包的一个非常有用的特点是能够从公开的父公钥中派生子公钥,而不需要拥有私钥。这为我们提供了两种派生子公钥的方法:从子私钥派生,或直接从父公钥派生。
因此,可以使用扩展公钥导出HD钱包结构分支中的所有 公钥(只有公钥)。
此快捷方式可用于创建非常安全的公钥 - 部署中的服务器或应用程序只有扩展公钥的副本,没有任何私钥。这种部署可以产生无限数量的公钥和以太坊地址, 但无法花费发送到这些地址的任何资金。与此同时,在另一个更安全的服务器上,扩展私钥可以导出所有相应的私钥来签署交易并花费金钱。
此解决方案的一个常见应用是在为电子商务应用程序提供服务的Web服务器上安装扩展公钥。网络服务器可以使用公钥派生函数为每个交易 (例如,针对客户购物车)创建新的以太坊地址。Web服务器将不会有任何易被盗的私钥。如果没有HD钱包,唯一的方法就是在单独的安全 服务器上生成数千个以太坊地址,然后将其预先加载到电子商务服务器上。这种方法很麻烦,需要不断的维护以确保电子商务服务器不会“用完”密钥。
此解决方案的另一个常见应用是冷钱包或硬件钱包。在这种情况下,扩展私钥可以存储在硬件钱包中,而扩展公钥可以保持在线。用户可以随意创建“接收”地址,而私钥可以安全地在离线状态下存储。要花费资金,用户可以在离线签署的以太坊客户端上使用扩展私钥或在硬件钱包设备上签署交易。
# 强化子密钥派生
从xpub派生公钥的分支是非常有用的,但它带有潜在风险。访问xpub不能访问子私钥。但是,因为xpub包含链码,所以如果某个子私钥已知,或者以某种方式泄漏, 则可以与链码一起使用,以派生所有其他子私钥。一个泄露的子私钥和一个父链码一起揭示了所有子私钥。更糟的是,可以使用子私钥和父链码来推导父私钥。
为了应对这种风险,HD钱包使用一种称为 强化派生 _hardened derivation_的替代派生函数,该函数“破坏”父公钥和子链码之间的关系。强化派生函数使用 父私钥来派生子链码,而不是父公钥。这会在父/子序列中创建一个“防火墙”,链码不能用于危害父代或同级私钥。
简而言之,如果你想使用xpub的便利来派生公钥的分支,而不会让自己面临泄漏链码的风险,所以应该从强化父项而不是普通父项派生。作为最佳做法,主密钥的1级 子密钥级始终通过强化派生派生,以防止主密钥受到破坏。
# 正常和强化派生的索引号
BIP-32派生函数中使用的索引号是一个32位整数。为了便于区分通过常规派生函数派生的密钥与通过强化派生函数派生的密钥,该索引号分为两个部分。 0到 (0x0到0x7FFFFFFF)之间的索引号仅用于常规派生。 和 (0x80000000至0xFFFFFFFF)之间的索引号仅用于强化派生。 因此,如果索引号小于 ,则子项是常规的,如果索引号等于或大于 ,则子项是强化的。
为了使索引号更容易阅读和展示,强化子项的索引号从零开始展示,但带有一个主要符号。第一个正常子密钥展示为0,而第一个强化子密钥 (索引0x80000000)展示为0++'++。然后,按顺序,第二个强化子密钥将具有索引0x80000001,并将展示为1++'++,依此类推。 当你看到HD钱包索引i++'++时,表示 。
# HD钱包密钥标识符(路径)
HD钱包中的密钥使用“路径”命名约定来标识,树的每个级别都用斜杠(/)字符分隔(参见 [ HD wallet path examples])。从主密钥派生的私钥以“m”开头。 从主公钥派生的公钥以“M”开始。因此,主私钥的第一个子私钥为m/0。第一个子公钥是M/0。第一个孩子的第二个孩子是m/0/1,依此类推。
从右向左读取一个密钥的“祖先”,直到你到达从派生出它的主密钥。例如,标识符 m/x/y/z 描述了密钥 m/x/y 的第z个子密钥,密钥 m/x/y 是密钥 m/x 的第y个子密钥,密钥 m/x 是 m 的第 x 个子密钥。
Table 5. HD wallet path examples
HD path | Key described |
m/0 | The first (0) child private key from the master private key (m) |
m/0/0 | The first grandchild private key of the first child (m/0) |
m/0'/0 | The first normal grandchild of the first hardened child (m/0') |
m/1/0 | The first grandchild private key of the second child (m/1) |
M/23/17/0/0 | The first great-great-grandchild public key of the first great-grandchild of the 18th grandchild of the 24th child |
# HD钱包树状结构导航
HD钱包树结构提供了巨大的灵活性。每个父扩展密钥可以有40亿子密钥:20亿正常子密钥和20亿强化子密钥。这些子密钥中的每一个又可以有另外40亿子密钥, 以此类推。这棵树可以像你想要的一样深,无限的世代。然而,这些灵活性,使得在这个无限树中导航变得非常困难。在实现之间转移HD钱包尤其困难, 因为内部组织分支和子分支的可能性是无穷无尽的。
通过为HD钱包的树状结构创建一些标准,两个BIP为这种复杂性提供了解决方案。BIP-43建议使用第一个强化子密钥作为表示树结构“目的”的特殊标识符。 基于BIP-43,HD钱包应该只使用树的一个1级分支,索引号通过定义其目的来标识树的其余部分的结构和名称空间。例如,仅使用分支m/i++'++/的HD钱包 表示特定目的,而该目的由索引号“i”标识。
扩展该规范,BIP-44提出了一个多币种多帐户结构作为BIP-43下的“目的”号码44'
。遵循BIP-44的HD钱包通过仅使用树的一个分支的事实来标识:m / 44'/。
BIP-44指定了包含五个预定义层级的结构
m / purpose' / coin_type' / account' / change / address_index
第一级“purpose”始终设置为44'
。第二级“coin_type”指定加密货币类型,允许多货币HD钱包,其中每种货币在第二级下具有其自己的子树。
标准文件中定义了几种货币,称为SLIP0044:
https://github.com/satoshilabs/slips/blob/master/slip-0044.md (opens new window)
一些例子: Ethereum 是 m/44++'++/60++'++, Ethereum Classic is m/44++'++/61++'++, Bitcoin 是 m/44++'++/0++'++, 所有货币的 Testnet 是 m/44++'++/1++'++.
树的第三层“account”, 允许用户将他们的钱包分割成逻辑上的子账户,用于会计或组织管理目的。例如HD钱包可能包含两个以太坊“账户”: m/44++'++/60++'++/0++'++ 和 m/44++'++/60++'++/1++'++. 每个账户都是自己的子树的根。
由于BIP-44最初是为比特币创建的,因此它包含一个在以太坊世界中不相关的“怪癖”。在路径的第四层“change”时,HD钱包有两个子树, 一个用于创建接收地址,另一个用于创建零钱地址。以太坊只使用“接收”路径,因为没有零钱地址这样的东西。请注意,虽然以前的层级使用强化派生, 但此层级使用正常派生。这是为了允许树的这个层级导出扩展公钥在非安全环境中使用。可用地址由HD钱包作为第四级的孩子派生, 使树的第五级成为“address_index”。例如,在主账户中以太坊付款的第三个接收地址为M/44++'++/60++'++/0++'++/0/2。 [BIP-44 HD wallet structure examples ] 展示了几个例子。 Table 6. BIP-44 HD wallet structure examples
HD path | Key described |
M/44'/60'/0'/0/2 | The third receiving public key for the primary Ethereum account |
M/44'/0'/3'/1/14 | The fifteenth change-address public key for the fourth Bitcoin account |
m/44'/2'/0'/0/1 | The second private key in the Litecoin main account, for signing transactions |
# 第七章 交易
交易是由外部所有帐户发起的签名消息,由以太坊网络传输,并在以太坊区块链上进行记录(挖掘)。在这个基本定义背后, 有很多令人惊讶和着迷的细节。看待交易的另一种方式是,它们是唯一可触发状态更改或导致合约在EVM中执行的东西。 以太坊是一个全球的单实例状态机器,交易是唯一可以让状态机“运动”,改变状态的东西。合约不会自行运行。 以太坊不会在后台运行。一切都始于交易。
在本节中,我们将剖析交易,展示它们的工作方式,并了解详细信息。
# 交易的结构
首先让我们来看看交易的基本结构,因为它是在以太坊网络上进行序列化和传输的。接收序列化交易的每个客户端和应用程序将使用 其自己的内部数据结构将其存储在内存中,还会使用网络序列化交易本身中不存在的元数据进行修饰。交易的网络序列化是交易结构 的唯一通用标准。
交易是一个序列化的二进制消息,其中包含以下数据:
nonce
由始发EOA(外部所有账户)发出的序列号,用于防止消息重播。
gas price
发起人愿意支付的gas价格(以wei为单位)。
start gas
发起人愿意支付的最大gas量。
to
目标以太坊地址。
value
发送到目标地址的ether数量。
data
变长二进制数据。
v,r,s
始发EOA的ECDSA签名的三个组成部分。
交易消息的结构使用递归长度前缀(RLP)编码方案(参见 [rlp] )进行序列化,该方案是专门为以太坊中准确和字节完美的数据 序列化而创建的。以太坊中的所有数字都被编码为大端序整数,其长度为8位的倍数。
请注意,字段的标签(“to”,“start gas”等)在这里是为清楚起见而显示,但不是包含字段值的RLP编码交易序列化数据的一部分。 通常,RLP不包含任何字段分隔符或标签。RLP的长度前缀用于标识每个字段的长度。因此,超出定义长度的任何内容都属于结构中的 下一个字段。
虽然这是实际传输的交易结构,但大多数内部表示和用户界面可视化都使用来自交易或区块链的附加信息来修饰它。
例如,你可能会注意到没有表示发起人EOA的地址的“from
”数据。EOA的公钥可以很容易地从ECDSA签名的v,r,s
组成部分中
派生出来。EOA的地址又可以很容易地从公钥中派生出来。当你看到显示“from”字段的交易时,是该交易所用的软件添加了该字段。
客户端软件经常添加到交易中的其他元数据包括块编号(被挖掘之后生成)和交易ID(计算出的哈希)。同样,这些数据来源于交易,
但不是交易信息本身的一部分。
# 交易的随机数(nonce)
nonce是交易中最重要和最少被理解的组成部分之一。黄皮书中的定义(见 [yellow_paper] )写道:
nonce:与此地址发送的交易数量相等的标量值,或者,对于具有关联代码的帐户,表示此帐户创建的合约数量。
严格地说,nonce是始发地址的一个属性(它只在发送地址的上下文中有意义)。但是,该nonce并未作为账户状态的 一部分显式存储在区块链中。相反,它是根据来源于此地址的已确认交易的数量动态计算的。
nonce值也用于防止帐户余额的错误计算。例如,假设一个账户有10个以太的余额,并且签署了两个交易,都花费6个ether, 分别具有nonce 1和nonce 2。这两笔交易中哪一笔有效?在以太坊这样的分布式系统中,节点可能无序地接收交易。 nonce强制任何地址的交易按顺序处理,不管间隔时间如何,无论节点接收到的顺序如何。这样,所有节点都会计算相同的余额。 支付6以太币的交易将被成功处理,账户余额减少到4 ether。无论什么时候收到,所有节点都认为与带有nonce 2的交易无效。 如果一个节点先收到nonce 2的交易,会持有它,但在收到并处理完nonce 1的交易之前不会验证它。
使用nonce确保所有节点计算相同的余额,并正确地对交易进行排序,相当于比特币中用于防止“双重支付”的机制。但是,因为 以太坊跟踪账户余额并且不会单独跟踪独立的币(在比特币中称为UTXO),所以只有在账户余额计算错误时才会发生“双重支付”。 nonce机制可以防止这种情况发生。
# 跟踪nonce
实际上,nonce是源自帐户的 已确认(已开采)交易数量的最新计数。要找到nonce是什么,你可以询问区块链,例如通过 web3界面:
Retrieving the transaction count of our example address
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f")
40
2
TIP
该nonce是一个基于零的计数器,意味着第一个交易的nonce是0.在 [nonce_getTransactionCount]中, 我们有一个交易的计数为40,这意味着从0到39nonce已经被看到。下一个交易的nonce将是40。
你的钱包将跟踪其管理的每个地址的nonce。这很简单,只要你只是从单一点发起交易即可。假设你正在编写自己的钱包 软件或其他一些发起交易的应用程序。你如何跟踪nonce?
当你创建新的交易时,你将分配序列中的下一个nonce。但在确认之前,它不会计入 getTransactionCount
的总数。
不幸的是,如果我们连续发送一些交易,getTransactionCount
函数会遇到一些问题。有一个已知的错误,
其中 getTransactionCount
不能正确计数待处理(pending)交易。我们来看一个例子:
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
40
web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
41
web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
41
web3.eth.sendTransaction({from: web3.eth.accounts[0], to: "0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.toWei(0.01, "ether")});
web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", "pending")
41
2
3
4
5
6
7
8
9
10
11
如你所见,我们发送的第一笔交易将交易计数增加到了41,显示了待处理交易。但是当我们连续发送3个更多的交易时,
getTransactionCount
调用并没有正确计数。它只计算一个,即使在mempool中有3个待处理交易。如果我们等待几秒钟,
一旦块被挖掘,getTransactionCount
调用将返回正确的数字。但在此期间,虽然有多项交易待处理,但对我们无帮助。
当你构建生成交易的应用程序时,无法依赖 getTransactionCount
处理未完成的交易。只有在待处理和已确认相同
(所有未完成的交易都已确认)时,才能信任 getTransactionCount
的输出以开始你的nonce计数器。此后,
请跟踪你的应用中的nonce,直到每笔交易被确认。
Parity的JSON RPC接口提供 parity_nextNonce
函数,该函数返回应在交易中使用的下一个nonce。parity_nextNonce
函数可以正确地计算nonce,即使你连续快速构建多个交易,但没有确认它们。
[[parity_curl]] Parity 有一个用于访问JSON RPC接口的Web控制台,但在这里我们使用命令行HTTP客户端来访问它:
curl --data '{"method":"parity_nextNonce","params":["0x9e713963a92c02317a681b9bb3065a8249de124f"],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST localhost:8545
{"jsonrpc":"2.0","result":"0x32","id":1}
2
3
# nonce的间隔,重复的nonce和确认
如果你正在以编程方式创建交易,跟踪nonce是十分重要的,特别是如果你同时从多个独立进程执行此操作。
以太坊网络根据nonce顺序处理交易。这意味着如果你使用nonce 0
传输一个交易,然后传输一个具有nonce 2
的交易,则第二个交易将不会被挖掘。它将存储在mempool中,以太坊网络等待丢失的nonce出现。所有节点都会假设缺少的nonce只是延迟了,具有nonce 2
的交易被无序地接收到。
如果你随后发送一个丢失的nonce 1
的交易,则交易(交易1
和2
)将被开采。一旦你填补了空白,网络可以挖掘它在mempool中的失序交易。
这意味着如果你按顺序创建多个交易,并且其中一个交易未被挖掘,则所有后续交易将“卡住”,等待丢失的事件。交易可以在nonce序列中产生无意的“间隙”,比如因为它无效或gas不足。为了让事情继续进行,你必须传输一个具有丢失的nonce的有效交易。
另一方面,如果你不小心重复一个nonce,例如传输具有相同nonce的两个交易,但收件人或值不同,则其中一个将被确认,另一个将被拒绝。哪一个被确认将取决于它们到达第一个接收它们的验证节点的顺序。
正如你所看到的,跟踪nonce是必要的,如果你的应用程序没有正确地管理这个过程,你会遇到问题。不幸的是,如果你试图并发地做到这一点,事情会变得更加困难,我们将在下一节中看到。
# 并发,交易的发起和随机数
并发是计算机科学的一个复杂方面,有时候它会突然出现,特别是在像Ethereum这样的去中心化/分布式实时系统中。
简单来说,并发是指多个独立系统同时进行计算。这些可以在相同的程序(例如线程)中,在相同的CPU(例如多进程)上, 或在不同的计算机(即分布式系统)上。按照定义,以太坊是一个允许操作(节点,客户端,DApps)并发的系统, 但是强制实施一个单一的状态(例如,对于每个开采的区块只有一个公共/共享状态的系统)。
现在,假设我们有多个独立的钱包应用程序正在从同一个地址或同一组地址生成交易。这种情况的一个例子是从热钱包进行提款 的交易所。理想情况下,你希望有多台计算机处理提款,以便它不会成为瓶颈或单点故障。然而,这很快就会成为问题, 因为有多台计算机生产提款会导致一些棘手的并发问题,其中最重要的是选择nonce。多台电脑如何从同一个热钱包账户协调生成, 签署和广播交易?
你可以使用一台计算机根据先到先得的原则为签署交易的计算机分配nonce。但是,这台电脑现在是可能故障的单点。更糟糕的是, 如果分配了多个nonce,并且其中一个从没有被使用(因为计算机处理具有该nonce的交易失败),所有后续交易都会卡住。
你可以生成交易,但不为它们签名或为其分配临时值。然后将它们排队到一个签名它们的节点,并跟踪随机数。再次, 你有了一个可能故障的单点。nonce的签名和跟踪是你的操作的一部分,可能在负载下变得拥塞,而未签名交易的生成 是你并不需要实现并行化的部分。你有并发性,但不是在过程中任何有用的部分。
最后,除了跟踪独立进程中的账户余额和交易确认的难度之外,这些并发问题迫使大多数实现朝着避免并发和创建瓶颈进行, 诸如单个进程处理交易所中的所有取款交易。
# 交易gas
我们在 [gas] 中详细讨论_gas_。但是,让我们介绍有关交易的 gasPrice
和 startGas
字段的一些基本知识。
gas是以太坊的燃料。gas不是ether,它是独立的虚拟货币,有相对于ether的汇率。以太坊使用gas来控制交易可以花费的资源量, 因为它将在全球数千台计算机上处理。开放式(图灵完备的)计算模型需要某种形式的计量,以避免拒绝服务攻击或无意中的资源 吞噬交易。
gas与ether分离,以保护系统免受随着ether价值快速变化而产生的波动。
交易中的 gasPrice
字段允许交易创建者设置每个单位的gas的汇率。gas价格以每单位gas多少 wei
测量。例如,在我们
最近一个例子创建的交易中,我们的钱包已将 gasPrice
设置为 3 Gwei
(3千兆,30亿wei)。
网站 ethgasstation.info
提供有关以太坊主网络当前gas价格以及其他相关gas指标的信息:
https://ethgasstation.info/ (opens new window)
钱包可以在他们发起的交易中调整 gasPrice
,以更快地确认(挖掘)交易。gasPrice
越高,交易可能被验证的速度越快。
相反,较低优先级的交易可能会降低他们愿意为gas支付的价格,导致确认速度减慢。可以设置的最低gasPrice
为零,
这意味着免费的交易。在区块空间需求低的时期,这些交易将被开采。
TIP
最低可接受的gasPrice为零。这意味着钱包可以产生完全免费的交易。根据能力的不同,这些可能永远不会被开采, 但协议中没有任何禁止免费交易内容。你可以在以太坊区块链中找到几个此类交易成功开采的例子。
web3界面通过计算几个区块的中间价格来提供gasPrice建议:
truffle(mainnet)> web3.eth.getGasPrice(console.log)
truffle(mainnet)> null BigNumber { s: 1, e: 10, c: [ 10000000000 ] }
2
与gas有关的第二个重要领域是 startGas
。这在 [gas] 中有更详细的解释。简单地说,startGas
定义交易发起人愿意
花费多少单位完成交易。对于简单付款,意味着将ether从一个EOA转移到另一个EOA的交易,所需的gas量固定为21,000个gas单位。
要计算需要花费多少ether,你需要将你愿意支付的 gasPrice
乘以21,000:
truffle(mainnet)> web3.eth.getGasPrice(function(err, res) {console.log(res*21000)} )
truffle(mainnet)> 210000000000000
2
如果你的交易的目的地址是合约,则可以估计所需的gas量,但无法准确确定。这是因为合约可以评估不同的条件,导致不同的执行 路径和不同的gas成本。这意味着合约可能只执行简单的计算或更复杂的计算,具体取决于你无法控制且无法预测的条件。为了说明 这一点,我们使用一个颇为人为的例子:每次调用合约时,它会增加一个计数器,并在第100次(仅)计算一些复杂的事情。如果 你调用99次合约,会发生一件事情,但在第100次调用时,会发生完全不同的事情。你要支付的gas数量取决于交易开采前有多少 其他交易调用了该功能。也许你的估计是基于第99次交易,并且在你的交易被开采之前,其他人已经调用了99次合约。现在,你是 第100个要调用的交易,计算工作量(和gas成本)要高得多。
借用以太坊使用的常见类比,你可以将startGas视为汽车中的油箱(你的汽车是交易)。你认为它需要旅行(验证交易所需的计算) ,就用尽可能多的gas填满油箱。你可以在某种程度上估算金额,但你的旅程可能会有意想不到的变化,例如分流(更复杂的执行路径) ,这会增加燃油消耗。
然而,与燃料箱的比较有些误导。这更像是一家加油站公司的信用账户,根据你实际使用的gas量,在旅行完成后支付。当你传输你的交易时,
首先验证步骤之一是检查它源自的帐户是否有足够的金额支付 gasPrice * startGas
费用。但是,在交易执行结束之前,金额实际上并未
从你的帐户中扣除。只收取你最终交易实际消耗的天然气,但在发送交易之前,你必须有足够的余额用于你愿意支付的最高金额。
# 交易的接收者
交易的收件人在to
字段中指定。这包含一个20字节的以太坊地址。地址可以是EOA或合约地址。
以太坊没有进一步验证这个字段。任何20字节的值都被认为是有效的。如果20字节的值对应于没有相应私钥的地址,或没有相应的合约,则该交易仍然有效。 以太坊无法知道某个地址是否是从公钥(从私钥导出的)正确导出的。
WARNING
以太坊不能也不会验证交易中的接收者地址。你可以发送到没有相应私钥或合约的地址,从而“燃烧”ether,使其永远不会被花费。验证应该在用户界面层级完成。
发送一个交易到一个无效的地址会_燃烧_发送的ether,使其永远不可访问(不可花费),因为不能生成用来使用它的签名。假定地址验证发生在用户界面级别 (参见 [eip-55] 或 [icap])。事实上,有很多合理的理由来燃烧ether,包括作为游戏理论,来抑制支付通道和其他智能合约作弊。
# 交易的价值和数据
交易的主要“负载”包含在两个字段中:value
和 data
。交易可以同时具有value和data,只有value,只有data,或没有value和data。所有四种组合都是有效的。
只有value的交易是 支付 payment。只有data的交易是 调用 invocation。既没有value也没有data的交易,这可能只是浪费gas!但它仍然有可能。
让我们尝试所有上述组合:
首先,我们从我们的钱包中设置源地址和目标地址,以使演示更易于阅读:
Set the source and destination addresses
src = web3.eth.accounts[0];
dst = web3.eth.accounts[1];
2
# 有value的交易(支付),没有data
Value, no data
web3.eth.sendTransaction({from: src, to: dst, value: web3.toWei(0.01, "ether"), data: ""});
我们的钱包显示确认屏幕,指示要发送的value,并且没有data:

Figure 1. Parity wallet showing a transaction with value, but no data
# 有value(支付)data的交易
Value and data
web3.eth.sendTransaction({from: src, to: dst, value: web3.toWei(0.01, "ether"), data: "0x1234"});
我们的钱包显示一个确认屏幕,指示要发送的value和data:

Figure 2. Parity wallet showing a transaction with value and data
# 0 value 的交易,只有数据
No value, only data
web3.eth.sendTransaction({from: src, to: dst, value: 0, data: "0x1234"});
我们的钱包显示一个确认屏幕,指示value为0并显示data:

Figure 3. Parity wallet showing a transaction with no value, and no data
# 既没有value(支付)也没有data的交易
No value, no data
web3.eth.sendTransaction({from: src, to: dst, value: 0, data: ""}));
我们的钱包显示确认屏幕,指示0 value并且没有data:

Figure 4. Parity wallet showing a transaction with no value, and no data
# 将value传递给EOA和合约
当你构建包含 value
的以太坊交易时,它等同于_payment_。根据目的地址是否为合约,这些交易行为会有所不同。
对于EOA地址,或者更确切地说,对于未在区块链中注册为合约的任何地址,以太坊将记录状态更改,并将你发送的value添加到地址的余额中。
如果地址之前没有被查看过,则会创建地址并将其余额初始化为你的付款value
。
如果目标地址(to
)是合约,则EVM将执行合约并尝试调用你的交易的 data
中指定的函数(参见 [invocation] )。如果你的交易中没有 data
,
那么EVM将调用目标合约的 fallback 函数,如果该函数是payable,则将执行该函数以确定下一步该做什么。
合约可以通过在调用付款功能时立即抛出异常或由付款功能中编码的条件确定来拒绝收款。如果付款功能成功终止(没有意外),则更新合约状态以反映合约 的ether余额增加。
# 将数据传输到EOA或合约
当你的交易包含data
时,它很可能是发送到合约地址的。这并不意味着你无法向EOA发送data
。事实上,你可以做到这一点。但是,在这种情况下,
data
的解释取决于你用来访问EOA的钱包。大多数钱包会忽略它们控制的EOA交易中收到的任何data
。将来,可能会出现允许钱包以合约的方式解释
data
编码的标准,从而允许交易调用在用户钱包内运行的函数。关键的区别在于,与合约执行不同,EOA对data的任何解释都不受以太坊共识规则的约束。
现在,假设你的交易是向合约地址提供 data
。在这种情况下,data
将被EVM解释为 函数调用 function invocation,调用指定的函数并将
任何编码参数传递给该函数。
发送到合约的 data
是一个十六进制序列化的编码:
函数选择器(function selector):: 函数_prototype_的Keccak256哈希的前4个字节。这使EVM能够明确地识别你希望调用的功能。
函数参数:: 函数的参数,根据EVM定义的各种基本类型的规则进行编码。
我们来看一个简单的例子,它来自我们的[solidity_faucet_example]。在Faucet.sol
中,我们为取款定义了一个函数:
function withdraw(uint withdraw_amount) public {
withdraw函数的_prototype_被定义为包含函数名称的字符串,随后是括号中括起来的每个参数的数据类型,并用单个逗号分隔。函数名称是withdraw
,
它只有一个参数是uint(它是uint256的别名)。所以withdraw
的原型将是:
withdraw(uint256)
我们来计算这个字符串的Keccak256哈希值(我们可以使用truffle控制台或任何JavaScript web3控制台来做到这一点):
web3.sha3("withdraw(uint256)");
'0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f'
2
散列的前4个字节是 0x2e1a7d4d
。这是我们的“函数选择器”的值,它会告诉EVM我们想调用哪个函数。
接下来,让我们计算一个值作为参数 withdraw_amount
传递。我们要取款0.01 ether。我们将它编码为一个十六进制序列化的大端序无符号256位整数,
以wei为单位:
withdraw_amount = web3.toWei(0.01, "ether");
'10000000000000000'
withdraw_amount_hex = web3.toHex(withdraw_amount);
'0x2386f26fc10000'
2
3
4
现在,我们将函数选择器添加到这个参数上(填充为32字节):
2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000
这就是我们的交易的 data
,调用 withdraw
函数并请求0.01 ether作为 withdraw_amount
。
# 特殊交易:合约注册
有一种特殊的带有data,没有value的交易。表示注册一个新的合约。合约登记交易被发送到一个特殊的目的地地址,即零地址。简而言之,
合约注册交易中的to
字段包含地址 0x0
。该地址既不代表EOA(没有相应的私人/公共密钥对)也不代表合约。它永远不会花费ether或启动交易。
它仅用作目的地,具有“注册此合约”的特殊含义。
尽管零地址仅用于合约注册,但它有时会收到来自各个地址的付款。对此有两种解释:无论是偶然的,导致ether的丧失,还是故意的_ ether燃烧_ (见[burning_ether])。如果你想进行有意识的ether燃烧,你应该向网络明确你的意图,并使用专门指定的燃烧地址:
0x000000000000000000000000000000000000dEaD
WARNING
发送至合约注册地址 0x0
或指定燃烧地址 0x0 ... dEaD
的任何ether将变得不可消费并永远丢失。
合约注册交易不应包含ether value,只能包含合约编译字节码的data。此次交易的唯一影响是注册合约。
作为例子,我们可以发布 [intro] 中使用的 Faucet.sol
。合约需要编译成二进制十六进制表示。这可以用Solidiy编译器完成。
> solc --bin Faucet.sol
======= Faucet.sol:Faucet =======
Binary:
6060604052341561000f57600080fd5b60e58061001d6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d146041575b005b3415604b57600080fd5b605f60048080359060200190919050506061565b005b67016345785d8a00008111151515607757600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050151560b657600080fd5b505600a165627a7a72305820d276ddd56041f7dc2d2eab69f01dd0a0146446562e25236cf4ba5095d2ee802f0029
2
3
4
相同的信息也可以从Remix在线编译器获得。 现在我们可以创建交易。
> src = web3.eth.accounts[0];
> faucet_code = "0x6060604052341561000f57600080fd5b60e58061001d6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d146041575b005b3415604b57600080fd5b605f60048080359060200190919050506061565b005b67016345785d8a00008111151515607757600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050151560b657600080fd5b505600a165627a7a72305820d276ddd56041f7dc2d2eab69f01dd0a0146446562e25236cf4ba5095d2ee802f0029"
> web3.eth.sendTransaction({from: src, data: faucet_code, gas: 113558, gasPrice: 200000000000})
"0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b"
2
3
4
5
6
不需要指定to
参数,将使用默认的零地址。你可以指定 gasPrice
和 gas
限制。
一旦合约被开采,我们可以在etherscan区块浏览器上看到它。

Figure 5. Etherscan showing the contract successully minded
你可以查看交易的接收者以获取有关合约的信息。
> eth.getTransactionReceipt("0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b");
{
blockHash: "0x6fa7d8bf982490de6246875deb2c21e5f3665b4422089c060138fc3907a95bb2",
blockNumber: 3105256,
contractAddress: "0xb226270965b43373e98ffc6e2c7693c17e2cf40b",
cumulativeGasUsed: 113558,
from: "0x2a966a87db5913c1b22a59b0d8a11cc51c167a89",
gasUsed: 113558,
logs: [],
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
status: "0x1",
to: null,
transactionHash: "0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b",
transactionIndex: 0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在这里我们可以看到合约的地址。我们可以按照 [data_EOA] 所示,从合约发送和接收资金。
> contract_address = "0xb226270965b43373e98ffc6e2c7693c17e2cf40b"
> web3.eth.sendTransaction({from: src, to: contract_address, value: web3.toWei(0.1, "ether"), data: ""});
"0x6ebf2e1fe95cc9c1fe2e1a0dc45678ccd127d374fdf145c5c8e6cd4ea2e6ca9f"
> web3.eth.sendTransaction({from: src, to: contract_address, value: 0, data: "0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000"});
"0x59836029e7ce43e92daf84313816ca31420a76a9a571b69e31ec4bf4b37cd16e"
2
3
4
5
6
7
8
过一段时间,这两个交易都可以在ethescan上看到

Figure 6. Etherscan showing the transactions for sending and receiving funds
# 数字签名
到目前为止,我们还没有深入探讨“数字签名”的细节。在本节中,我们将探讨数字签名是如何工作的,以及如何在不泄露私钥的情况下提供私钥所有权的证明。
# 椭圆曲线数字签名算法(ECDSA)
以太坊中使用的数字签名算法是_Elliptic Curve Digital Signature Algorithm_,或_ECDSA_。ECDSA是用于基于椭圆曲线私钥/公钥对的数字签名的算法, 如 [elliptic_curve] 中所述。
数字签名在以太坊中有三种用途(请参阅下面的边栏)。首先,签名证明私钥的所有者,暗示着以太坊账户的所有者,已经授权支付ether或执行合约。其次, 授权的证明是_undeniable_(不可否认)。第三,签名证明交易数据在交易签名后没有也不能被任何人修改。
Wikipedia对“数字签名”的定义
数字签名是用于证明数字信息或文件真实性的数学方案。有效的数字签名使收件人有理由相信该信息是由已知的发件人(认证)创建的,发件人不能否认已发送的信息 (不可否认),并且信息在传输过程中未被更改(完整性) 。 来源: https://en.wikipedia.org/wiki/Digital_signature_ (opens new window)
# 数字签名如何工作
数字签名是一种数学签名,由两部分组成。第一部分是使用私钥(签名密钥)从消息(交易)中创建签名的算法。第二部分是允许任何人仅使用消息和公钥 来验证签名的算法。
# 创建数字签名
在以太坊实现的ECDSA中,被签名的“消息”是交易,或者更确切地说,来自交易的RLP编码数据的Keccak256哈希。签名密钥是EOA的私钥。结果是签名:
其中:
- _k_是签名私钥
- _m_是RLP编码的交易
- F~keccak256~ 是Keccak256哈希函数
- F~sig~ 是签名算法
- Sig 是由此产生的签名
更多关于ECDSA数学的细节可以在 [ecdsa_math] 中找到。
函数 产生一个由两个值组成的签名Sig
,通常称为R
和S
:
Sig = (R, S)
# 验证签名
要验证签名,必须有签名(R
和S
),序列化交易和公钥(与用于创建签名的私钥对应)。实质上,对签名的验证意味着“只有生成此公钥的私钥的所有者才能在此
交易上产生此签名。”
签名验证算法采用消息(交易的散列或其部分),签名者的公钥和签名(R
和S
值),如果签名对此消息和公钥有效,则返回TRUE。
# ECDSA数学
如前所述,签名由数学函数 创建,该函数生成由两个值_R_和_S_组成的签名。在本节中,我们将更详细地讨论函数 。
签名算法首先生成_ephemeral_(临时的)私钥/公钥对。在涉及签名私钥和交易哈希的转换之后,此临时密钥对用于计算_R_和_S_值。
临时密钥对由两个输入值生成:
- 一个随机数_q_,用作临时私钥
- 和椭圆曲线生成点_G_
从_q_和_G_开始,我们生成相应的临时公钥_Q_(以_Q = q * G_计算,与以太坊公钥的派生方式相同,参见[pubkey])。数字签名的_R_值就是临时公钥_Q_的x坐标。
然后,算法计算签名的_S_值,以便:
其中:
- _q_是临时私钥
- _R_是临时公钥的x坐标
- _k_是签名(EOA所有者)的私钥
- _m_是交易数据
- _p_是椭圆曲线的素数阶
验证是签名生成函数的反函数,使用_R_,S_值和公钥来计算一个值_Q,它是椭圆曲线上的一个点(签名创建中使用的临时公钥):
其中:
- _R_和_S_是签名值
- _K_是签名者(EOA所有者)的公钥
- _m_是被签名的交易数据
- _G_是椭圆曲线生成点
- _p_是椭圆曲线的素数阶
如果计算的点_Q_的x坐标等于_R_,则验证者可以断定该签名是有效的。
请注意,在验证签名时,私钥既不被知道也不会透露。
TIP
ECDSA必然是一门相当复杂的数学; 完整的解释超出了本书的范围。许多优秀的在线指南会一步一步地通过它:搜索“ECDSA explained”或尝试这一个: http://bit.ly/2r0HhGB[]。
# 实践中的交易签名
为了产生有效的交易,发起者必须使用椭圆曲线数字签名算法将数字签名应用于消息。当我们说“签署交易”时,我们实际上是指“签署RLP序列化交易数据的 Keccak256哈希”。签名应用于交易数据的哈希,而不是交易本身。
TIP
在#2,675,000块,Ethereum实施了“Spurious Dragon”硬分叉,除其他更改外,还推出了包括交易重播保护的新签名方案。这个新的签名方案在EIP-155中指定 (参见[eip155])。此更改会影响签名过程的第一步,在签名之前向交易添加三个字段(v,r,s)。
要在以太坊签署交易,发件人必须:
- 创建一个包含九个字段的交易数据结构:nonce,gasPrice,startGas,to,value,data,v,r,s
- 生成交易的RLP编码的序列化消息
- 计算此序列化消息的Keccak256哈希
- 计算ECDSA签名,用发起EOA的私钥签名散列
- 在交易中插入ECDSA签名计算出的
r
和s
值
[[raw_tx]]
# 原始交易创建和签名
让我们创建一个原始交易并使用 ethereumjs-tx
库对其进行签名。此示例的源代码位于GitHub存储库中的 raw_tx_demo.js
中:
[[raw_tx_demo_source]] .raw_tx_demo.js: Creating and signing a raw transaction in JavaScript
include::code/web3js/raw_tx/raw_tx_demo.js[]
在此处下载: https://github.com/ethereumbook/ethereumbook/blob/develop/code/web3js/raw_tx/raw_tx_demo.js
[[raw_tx_demo_run]] 运行示例代码:
$ node raw_tx_demo.js
RLP-Encoded Tx: 0xe6808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb348080
Tx Hash: 0xaa7f03f9f4e52fcf69f836a6d2bbc7706580adce0a068ff6525ba337218e6992
Signed Raw Transaction: 0xf866808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1ecebdb3480801ca0ae236e42bd8de1be3e62fea2fafac7ec6a0ac3d699c6156ac4f28356a4c034fda0422e3e6466347ef6e9796df8a3b6b05bed913476dc84bbfca90043e3f65d5224
2
3
4
[[raw_tx_eip155]]
# 用EIP-155创建原始交易
EIP-155“简单重播攻击保护”标准在签名之前指定了重播攻击保护(replay-attack-protected)的交易编码,其中包括交易数据中的_chain identifier_。 这确保了为一个区块链(例如以太坊主网)创建的交易在另一个区块链(例如Ethereum Classic或Ropsten测试网络)上无效。因此,在一个网络上广播的交易 不能在另一个网络上广播,因此得名“重放攻击保护”。
EIP-155向交易数据结构添加了三个字段 v
,r
和s
。r
和s
字段被初始化为零。这三个字段在编码和散列_之前_被添加到交易数据中。因此,三个附加字
段会更改交易的散列,稍后将应用签名。通过在被签名的数据中包含链标识符,交易签名可以防止任何更改,因为如果链标识符被修改,签名将失效。因此,
EIP-155使交易无法在另一个链上重播,因为签名的有效性取决于链标识符。
签名前缀字段v
被初始化为链标识符,其值为:
Chain | Chain ID |
Ethereum mainnet | 1 |
Morden (obsolete), Expanse | 2 |
Ropsten | 3 |
Rinkeby | 4 |
Rootstock mainnet | 30 |
Rootstock testnet | 31 |
Kovan | 42 |
Ethereum Classic mainnet | 61 |
Ethereum Classic testnet | 62 |
Geth private testnets | 1337 |
由此产生的交易结构被进行RLP编码,哈希和签名。签名算法也稍作修改,以在v
前缀中对链ID进行编码。
有关更多详细信息,请参阅EIP-155规范: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md (opens new window)
# 签名前缀值(v)和公钥恢复
如[tx_struct]所述,交易消息不包含任何“from”字段。这是因为发起者的公钥可以直接从ECDSA签名中计算出来。一旦你有公钥,你可以很容易地计算出地址。 恢复签名者公钥的过程称为_公钥恢复_。
给定 [ecdsa_math] 中计算的值 r
和 s
,我们可以计算两个可能的公钥。
首先,我们根据签名中的x坐标 r
值计算两个椭圆曲线点R和R^'^。有个两点,因为椭圆曲线在x轴上是对称的,所以对于任何值x
,在x轴的两侧有两个可能
的值适合曲线。
从 r
开始,我们也计算r^-1^这是 r
的倒数。
最后我们计算 z
,它是消息散列的最低位,其中n是椭圆曲线的阶数。
然后两个可能的公钥是:
和
其中:
- 和 是签名者公钥的两种可能性
- 是签名的
r
值的倒数 - s是签名的
s
值 - R和 R^'是临时公钥_Q_的两种可能性
- z是消息散列的最低位
- G是椭圆曲线生成点
为了使事情更有效率,交易签名包括一个前缀值 v
,它告诉我们两个可能的R值中哪一个是临时的公钥。如果 v
是偶数,那么R是正确的值。如果 v
是奇数,那么选择R^'^。这样,我们只需要计算R的一个值。
# 分离签名和传输(离线签名)
一旦交易被签署,它就可以传送到以太坊网络。创建,签署和广播交易的三个步骤通常发生在单个函数中,例如使用web3.eth.sendTransaction
。但是,
正如我们在[raw_tx]中看到的那样,你可以通过两个单独的步骤创建和签署交易。一旦你签署了交易记录,你就可以使用web3.eth.sendSignedTransaction
传输该交易记录,该方法采用十六进制编码的签名交易信息并在Ethereum网络上传输。
你为什么要分开交易的签署和传输?最常见的原因是安全:签名交易的计算机必须将解锁的私钥加载到内存中。传输的计算机必须连接到互联网并运行以太坊客户端 。如果这两个功能都在一台计算机上,那么你的在线系统上有私钥,这非常危险。分离签名和传输功能称为 离线签名 offline signing,是一种常见的安全措施。
根据你所需的安全级别,你的“离线签名”计算机可能与在线计算机存在不同程度的分离,从隔离和防火墙子网(在线但隔离)到完全脱机系统, 成为 气隙 _air-gapped_系统 。在气隙系统中根本没有网络连接 - 计算机与在线环境是“空气”隔离的。使用数据存储介质或(更好)网络摄像头和QR码将 交易记录到气隙计算机上,以签署交易。当然,这意味着你必须手动传输你想要签名的每个交易,不能批量化。
尽管没有多少环境可以利用完全气隙系统,但即使是小程度的隔离也具有显着的安全优势。例如,带防火墙的隔离子网只允许通过消息队列协议,可以提供大大 降低的攻击面,并且比在线系统上签名的安全性高得多。许多公司使用诸如ZeroMQ(0MQ)的协议,因为它为签名计算机提供了减少的攻击面。有了这样的设置, 交易就被序列化并排队等待签名。排队协议以类似于TCP套接字的方式将序列化的消息发送到签名计算机。签名计算机从队列中读取序列化的交易(仔细地), 使用适当的密钥应用签名,并将它们放置在传出队列中。传出队列将签名的交易传输到使用Ethereum客户端的计算机上,客户端将这些交易出队并传输。
[[tx_propagation]]
# 交易传播
以太坊网络使用“泛洪”路由协议。每个以太坊客户端,在_Peer-to-Peer(P2P)中作为_node,(理想情况下)构成_mesh_网络。没有网络节点是“特殊的”, 它们都作为平等的对等体。我们将使用术语“节点”来指代连接并参与P2P网络的以太坊客户端。
交易传播开始于创建(或从离线接收)签名交易的以太坊节点。交易被验证,然后传送到_直接_连接到始发节点的所有其他以太坊节点。平均而言,每个以太坊 节点保持与至少13个其他节点的连接,称为_邻居_。每个邻居节点在收到交易后立即验证交易。如果他们同意这是有效的,他们会保存一份副本并将其传播给 所有的邻居(除了它的邻居)。结果,交易从始发节点向外涟漪式地遍历网络,直到网络中的所有节点都拥有交易的副本。
几秒钟内,以太坊交易就会传播到全球所有以太坊节点。从每个节点的角度来看,不可能辨别交易的起源。发送给我们节点的邻居可能是交易的发起者,或者 可能从其邻居那里收到它。为了能够跟踪交易的起源或干扰传播,攻击者必须控制所有节点的相当大的百分比。这是P2P网络安全和隐私设计的一部分, 尤其适用于区块链。
# 记录到区块链中
虽然以太坊中的所有节点都是相同的对等节点,但其中一些节点由_矿工_操作,并将交易和数据块提供给_挖矿农场_,这些节点是具有高性能图形处理单元 (GPU)的计算机。挖掘计算机将交易添加到候选块,并尝试查找使得候选块有效的_Proof-of-Work_。我们将在[consensus]中更详细地讨论这一点。
不深入太多细节,有效的交易最终将被包含在一个交易块中,并记录在以太坊区块链中。一旦开采成块,交易还通过修改账户余额(在简单付款的情况下) 或通过调用改变其内部状态的合约来修改以太坊单例的状态。这些更改将与交易一起以交易_收据_ receipt 的形式记录,该交易也可能包含_事件_ events。我们将在 [evm] 中更详细地检查所有这些。
我们的交易已经完成了从创建到被EOA签署,传播以及最终采矿的旅程。它改变了单例的状态,并在区块链上留下了不可磨灭的印记。
# 多重签名(multisig)交易
如果你熟悉比特币的脚本功能,那么你就知道有可能创建一个比特币多重签名账户,该账户只能在多方签署交易时花费资金(例如2个或3个或4个签名)。 以太坊的价值交易没有多重签名的规定,尽管可以部署任意条件的任意合约来处理ether和代币的转让。
为了在多重签名情况下保护你的ether,将它们转移到多重签名合约中。无论何时你想将资金转入其他账户,所有必需的用户都需要使用常规钱包软件 将交易发送至合约,从而有效授权合约执行最终交易。
这些合约也可以设计为在执行本地代码或触发其他合约之前需要多个签名。该方案的安全性最终由多重签名合约代码确定。
讨论和 Grid+ 参考实现: https://blog.gridplus.io/toward-an-ethereum-multisig-standard-c566c7b7a3f6 (opens new window)
# 第八章 智能合约
# 智能合约
我们在 [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构建智能合约
来自维基百科:
[quote, "Wikipedia entry for Solidity"] Solidity是编写智能合约的“面向合约的”编程语言。它用于在各种区块链平台上实施智能合约。它由Gavin Wood,Christian Reitwiessner, Alex Beregszaszi,Liana Husikyan,Yoichi Hirai和几位以前的以太坊核心贡献者开发,以便在区块链平台(如以太坊)上编写智能合约。
Solidity由GitHub上的Solidity项目开发团队开发并维护:
https://github.com/ethereum/solidity (opens new window)
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 (opens new window)
在[install_solidity_ubuntu]中,我们将使用 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
2
3
一旦你安装了 solc
,运行以下命令来检查版本:
$ solc --version
solc, the solidity compiler commandline interface
Version: 0.4.21+commit.dfe3193c.Linux.g++
2
3
根据你的操作系统和要求,还有许多其他方式可以安装Solidity,包括直接从源代码编译。有关更多信息,请参阅
https://github.com/ethereum/solidity (opens new window)
# 开发环境
要在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 {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
从 [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
2
3
4
solc
产生的结果是一个可以提交给以太坊区块链的十六进制序列化二进制文件。
[[eth_contract_abi_sec]]
# 以太坊合约应用程序二进制接口(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"}]
2
3
4
如你所见,编译器会生成一个描述由 Faucet.sol
定义的两个函数的JSON对象。这个JSON对象可以被任何希望在部署时访问 Faucet
合约的应用程序使用。
使用ABI,应用程序(如钱包或DApp浏览器)可以使用正确的参数和参数类型构造调用 Faucet
中的函数的交易。例如,钱包会知道要调用函数withdraw
,
它必须提供名为 withdraw_amount
的 uint256
参数。钱包可以提示用户提供该值,然后创建一个编码它并执行withdraw
功能的交易。
应用程序与合约进行交互所需的全部内容是ABI以及合约的部署地址。
# 选择Solidity编译器和语言版本
正如我们在 [compile_Faucet_with_solc] 中看到的,我们的 Faucet
合约在Solidity 0.4.21版本中成功编译。但是如果我们使用了不同版本的Solidity
编译器呢?语言仍然不断变化,事情可能会以意想不到的方式发生变化。我们的合约非常简单,但如果我们的程序使用了仅添加到Solidity版本0.4.19
中的功能,
并且我们尝试使用0.4.18
进行编译,该怎么办?
为了解决这些问题,Solidity提供了一个_compiler指令_,称为_version pragma_,指示编译器程序需要特定的编译器(和语言)版本。我们来看一个例子:
pragma solidity ^0.4.19;
Solidity编译器读取版本编译指示,如果编译器版本与版本编译指示不兼容,将会产生错误。在这种情况下,我们的版本编译指出,这个程序可以由Solidity编译器编译,
最低版本为0.4.19
。但是,符号^表示我们允许编译任何_minor修订版_在0.4.19
之上的,例如0.4.20
,但不是0.5.0
(这是一个主要版本,不是小修订版) 。
Pragma指令不会编译为EVM字节码。它们仅由编译器用来检查兼容性。
让我们在我们的 Faucet
合约中添加一条编译指示。我们将命名新文件 Faucet2.sol
,以便在我们继续处理这些示例时跟踪我们的更改:
Faucet2.sol : Adding the version pragma to Faucet
// Version of Solidity compiler this program was written for
pragma solidity ^0.4.19;
// 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 {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
添加版本 pragma 是最佳实践,因为它避免了编译器和语言版本不匹配的问题。我们将探索其他最佳实践,并在本章中继续改进Faucet
合约。
# 使用Solidity编程
在本节中,我们将看看Solidity语言的一些功能。正如我们在 [intro] 中提到的,我们的第一份合约示例非常简单,并且在许多方面也存在缺陷。 我们将逐渐改进这个例子,同时学习如何使用Solidity。然而,这不会是一个全面的Solidity教程,因为Solidity相当复杂且快速发展。 我们将介绍基础知识,并为你提供足够的基础,以便能够自行探索其余部分。Solidity的完整文档可以在以下网址找到:
https://solidity.readthedocs.io/en/latest/ (opens new window)
# 数据类型
首先,我们来看看Solidity中提供的一些基本数据类型:
boolean (bool)
布尔值, true
或 false
, 以及逻辑操作符 ! (not), && (and), || (or), == (equal), != (not equal). ,
整数 (int/uint)
有符号 (int) 和 无符号 (uint) 整数,从 u/int8 到 u/int256以 8 bits 递增,没有大小后缀的话,表示256 bits。
定点数 (fixed/ufixed)
定点数, 定义为 u/fixedMxN,其中 M 是位大小(以8递增),N 是小数点后的十进制数的个数。
地址
20字节的以太坊地址。address
对象有 balance
(返回账户的余额) 和 transfer
(转移ether到该账户) 成员方法。
字节数组(定长)
固定大小的字节数组,定义为bytes1
到bytes32
。
字节数组 (动态)
动态大小的字节数组,定义为bytes
或string
。
enum
枚举离散值的用户定义类型。
struct
包含一组变量的用户定义的数据容器。
mapping
key => value
对的哈希查找表。
除上述数据类型外,Solidity还提供了多种可用于计算不同单位的字面值:
时间单位:: seconds
, minutes
, hours
, 和 days
可用作后缀,转换为基本单位 seconds
的倍数。
以太的单位:: wei
, finney
, szabo
, 和 ether
可用作后缀, 转换为基本单位 wei
的倍数。
到目前为止,在我们的Faucet
合约示例中,我们使用uint
(这是uint256
的别名),用于withdraw_amount
变量。
我们还间接使用了address
变量,它是msg.sender
。在本章中,我们将在示例中使用更多数据类型。
让我们使用单位的倍数之一来提高示例合约Faucet
的可读性。在withdraw
函数中,我们限制最大提现额,将数量限制表示为wei
,以太的基本单位:
require(withdraw_amount <= 100000000000000000);
这不是很容易阅读,所以我们可以通过使用单位倍数 ether
来改进我们的代码,以ether而不是wei表示值:
require(withdraw_amount <= 0.1 ether);
# 预定义的全局变量和函数
在EVM中执行合约时,它可以访问一组较小范围内的全局对象。这些包括 block
,msg
和 tx
对象。另外,
Solidity公开了许多EVM操作码作为预定义的Solidity功能。在本节中,我们将检查你可以从Solidity的智能合约中访问的变量和函数。
# 调用交易/消息上下文
msg
msg
对象是启动合约执行的交易(源自EOA)或消息(源自合约)。它包含许多有用的属性:**
msg.sender
我们已经使用过这个。它代表发起消息的地址。如果我们的合约是由EOA交易调用的,那么这是签署交易的地址。
msg.value
与消息一起发送的以太网值。
msg.gas
调用我们的合约的消息中留下的gas量。它已经被弃用,并将被替换为Solidity v0.4.21中的gasleft()函数。
msg.data
调用合约的消息中的数据。
msg.sig
数据的前四个字节,它是函数选择器。
Note
每当合约调用另一个合约时,msg
的所有属性的值都会发生变化,以反映新的调用者的信息。唯一的例外是在原始msg
上下文中运行另一个合约/库的代码的
delegatecall
函数。
# 交易上下文
tx.gasprice
发起调用的交易中的gas价格。
tx.origin
源自(EOA)的交易的完整调用堆栈。
# 区块上下文
block
包含有关当前块的信息的块对象。
block.blockhash(blockNumber)
指定块编号的块的哈希,直到之前的256个块。已弃用,并使用Solidity v.0.4.22中的blockhash()
函数替换。
block.coinbase
当前块的矿工地址。
block.difficulty
当前块的难度(Proof-of-Work)。
block.gaslimit
当前块的区块gas限制。
block.number
当前块号(高度)。
block.timestamp
矿工在当前块中放置的时间戳,自Unix纪元(秒)开始。
# 地址对象
任何地址(作为输入传递或从合约对象转换而来)都有一些属性和方法:
address.balance
地址的余额,以wei为单位。例如,当前合约余额是 address(this).balance
。
address.transfer(amount)
将金额(wei)转移到该地址,并在发生任何错误时抛出异常。我们在Faucet
示例中的msg.sender
地址上
使用了此函数,msg.sender.transfer()
。
address.send(amount)
类似于前面的transfer
, 但是失败时不抛出异常,而是返回false
。
address.call()
低级调用函数,可以用value
,data
构造任意消息。错误时返回false
。
address.delegatecall()
低级调用函数,保持发起调用的合约的msg
上下文,错误时返回false
。
# 内置函数
addmod, mulmod
模加法和乘法。例如,addmod(x,y,k)
计算 +pass:[ (x + y) % k]`。
keccak256, sha256, sha3, ripemd160
用各种标准哈希算法计算哈希值的函数。
ecrecover
从签名中恢复用于签署消息的地址。
# 合约的定义
Solidity的主要数据类型是_contract_对象,它在我们的Faucet
示例的顶部定义。与面向对象语言中的任何对象类似,合约是一个包含数据和方法的容器。
Solidity提供了另外两个与合约类似的对象:
interface
接口定义的结构与合约完全一样,只不过没有定义函数,它们只是声明。这种类型的函数声明通常被称为 桩 stub,
因为它告诉你有关函数的参数和返回值,没有任何实现。它用来指定合约接口,如果继承,每个函数都必须在子类中指定。
library
一个库合约是一个只能部署一次并被其他合约使用的合约,使用delegatecall
方法(见[solidity_address_object])。
# 函数
在合约中,我们定义了可以由EOA交易或其他合约调用的函数。在我们的Faucet
示例中,我们有两个函数:withdraw
和(未命名的)_fallback_函数。
函数使用以下语法定义:
function{nbsp}FunctionName([parameters]) {public|private|internal|external} [pure|constant|view|payable] pass:[[]modifiers] [returns{nbsp}(pass:[<]_return types_pass:[>])]
我们来看看每个组件:
FunctionName:: 定义函数的名称,用于通过交易(EOA),其他合约或同一合约调用函数。每个合约中的一个功能可以定义为不带名称的,在这种情况下, 它是_fallback_函数,在没有指定其他函数时调用该函数。fallback函数不能有任何参数或返回任何内容。
parameters:: 在名称后面,我们指定必须传递给函数的参数,包括名称和类型。在我们的Faucet
示例中,我们将uint withdraw_amount
定义为withdraw
函数的唯一参数。
下一组关键字 (public, private, internal, external) 指定了函数的_可见性_:
public
Public是默认的,这些函数可以被其他合约,EOA交易或合约内部调用。在我们的Faucet
示例中,这两个函数都被定义为public。
external
外部函数就像public一样,但除非使用关键字this作为前缀,否则它们不能从合约中调用。
internal
内部函数只能在合约内部"可见",不能被其他合约或EOA交易调用。他们可以被派生合约调用(继承的)。
private private函数与内部函数类似,但不能由派生的合约调用(继承的)。
请记住,术语 internal
和 private
有些误导性。公共区块链中的任何函数或数据总是_可见的_,意味着任何人都可以看到代码或数据。
以上关键字仅影响函数的调用方式和时机。
下一组关键字(pure, constant, view, payable)会影响函数的行为:
constant/view
标记为_view_的函数,承诺不修改任何状态。术语_constant_是_view_的别名,将被弃用。目前,编译器不强制执行_view_修饰器,只产生一个警告,
但这应该成为Solidity v0.5中的强制关键字。
pure
纯(pure)函数不读写任何变量。它只能对参数进行操作并返回数据,而不涉及任何存储的数据。纯函数旨在鼓励没有副作用或状态的声明式编程。
payable
payable函数是可以接受付款的功能。没有payable的函数将拒绝收款,除非它们来源于coinbase(挖矿收入)或 作为 SELFDESTRUCT
(合约终止)的目的地。在这些情况下,由于EVM中的设计决策,合约无法阻止收款。
正如你在Faucet
示例中看到的那样,我们有一个payable函数(fallback函数),它是唯一可以接收付款的函数。
# 合约构造和自毁
有一个特殊函数只能使用一次。创建合约时,它还运行 构造函数 constructor function(如果存在),以初始化合约状态。
构造函数与创建合约时在同一个交易中运行。构造函数是可选的。事实上,我们的Faucet
示例没有构造函数。
构造函数可以通过两种方式指定。到Solidity v.0.4.21,构造函数是一个名称与合约名称相匹配的函数:
Constructor function prior to Solidity v0.4.22
contract MEContract {
function MEContract() {
// This is the constructor
}
}
2
3
4
5
这种格式的难点在于如果合约名称被改变并且构造函数名称没有改变,它就不再是构造函数了。这可能会导致一些非常令人讨厌的,意外的并且很难注意到的错误。 想象一下,例如,如果构造函数正在为控制目的而设置合约的“所有者”。它不仅可以在创建合约时设置所有者,还可以像正常功能那样“可调用”,允许任何第三方 在合约创建后劫持合约并成为“所有者”。
为了解决构造函数的潜在问题,它基于与合约名称相同的名称,Solidity v0.4.22引入了一个constructor
关键字,它像构造函数一样运行,但没有名称。
重命名合约并不会影响构造函数。此外,更容易确定哪个函数是构造函数。看起来像这样:
pragma ^0.4.22
contract MEContract {
constructor () {
// This is the constructor
}
}
2
3
4
5
6
总而言之,合约的生命周期始于EOA或其他合约的创建交易。如果有一个构造函数,它将在相同的创建交易中调用,并可以在创建合约时初始化合约状态。
合约生命周期的另一端是 合约销毁 contract destruction。合约被称为SELFDESTRUCT
的特殊EVM操作码销毁。它曾经是SUICIDE
,但由于
该词的负面性,该名称已被弃用。在Solidity中,此操作码作为高级内置函数selfdestruct
公开,该函数采用一个参数:地址以接收合约帐户中剩余
的余额。看起来像这样:
selfdestruct(address recipient);
# 添加一个构造函数和selfdestruct到我们的Faucet
示例
我们在[intro]中引入的Faucet
示例合约没有任何构造函数或自毁函数。这是永恒的合约,不能从区块链中删除。让我们通过添加一个构造函数和
selfdestruct函数来改变它。我们可能希望自毁仅由最初创建合约的EOA来调用。按照惯例,这通常存储在称为owner
的地址变量中。我们的构造
函数设置所有者变量,并且selfdestruct函数将首先检查是否是所有者调用它。
首先是我们的构造函数:
// Version of Solidity compiler this program was written for
pragma solidity ^0.4.22;
// Our first contract is a faucet!
contract Faucet {
address owner;
// Initialize Faucet contract: set owner
constructor() {
owner = msg.sender;
}
[...]
2
3
4
5
6
7
8
9
10
11
12
13
14
我们已经更改了pragma指令,将v0.4.22指定为此示例的最低版本,因为我们使用的是仅存在于Solidity v.0.4.22中的constructor关键字。
我们的合约现在有一个名为owner
的address
类型变量。名称“owner”不是特殊的。我们可以将这个地址变量称为“potato”,仍然以相同的
方式使用它。名称owner
只是简单明了的目的和目的。
然后,作为合约创建交易的一部分运行的constructor函数将msg.sender
的地址分配给owner
变量。我们使用 withdraw
函数中的
msg.sender
来 标识提款请求的来源。然而,在构造函数中,msg.sender
是签署合约创建交易的EOA或合约地址。这是事实,因为这
是一个构造函数:它只运行一次,并且仅作为合约创建交易的结果。
好的,现在我们可以添加一个函数来销毁合约。我们需要确保只有所有者才能运行此函数,因此我们将使用require
语句来控制访问。看起
来像这样:
// Contract destructor
function destroy() public {
require(msg.sender == owner);
selfdestruct(owner);
}
2
3
4
5
如果其他人用 owner
以外的地址调用 destroy 函数,则将失败。但是,如果构造函数存储在 owner
中的地址调用它,合约将自毁,并
将剩余余额发送到 owner
地址。
# 函数修饰器
Solidity提供了一种称为_函数修饰器_的特殊类型的函数。通过在函数声明中添加修饰器名称,可以将修饰器应用于函数。修饰器函数通常用于
创建适用于合约中许多函数的条件。我们已经在我们的destroy
函数中有一个访问控制语句。让我们创建一个表达该条件的函数修饰器:
[[function_modifier_onlyowner]] .onlyOwner function modifier
modifier onlyOwner {
require(msg.sender == owner);
_;
}
2
3
4
在 [function_modifier_onlyowner] 中,我们看到函数修饰器的声明,名为onlyOwner
。此函数修饰器为其修饰的任何函数设置条件,要求
存储为合约的owner
的地址与交易的msg.sender
的地址相同。这是访问控制的基本设计模式,只允许合约的所有者执行具有onlyOwner
修饰器的任何函数。
你可能已经注意到我们的函数修饰器在其中有一个特殊的语法“占位符”,下划线后跟分号(pass:[_;]
)。此占位符由正在修饰的函数的代码替换。
本质上,修饰器“修饰”修饰过的函数,将其代码置于由下划线字符标识的位置。
要应用修饰器,请将其名称添加到函数声明中。可以将多个修饰器应用于一个函数,作为逗号分隔的列表,以声明的顺序应用。
让我们重新编写destroy
函数来使用onlyOwner
修饰器:
function destroy() public onlyOwner {
selfdestruct(owner);
}
2
3
函数修饰器的名称(onlyOwner
)位于关键字public
之后,并告诉我们destroy
函数由onlyOwner
修饰器修饰。基本上你可以把它写成:
“只有所有者才能销毁这份合约”。实际上,生成的代码相当于由onlyOwner
“包装” 的destroy
代码。
函数修饰器是一个非常有用的工具,因为它们允许我们为函数编写前提条件并一致地应用它们,使代码更易于阅读,因此更易于审计安全问题。它们 最常用于访问控制,如示例中的“function_modifier_onlyowner”,但功能很多,可用于各种其他目的。
在修饰函数内部,可以访问被修饰的函数的的所有可见符号(变量和参数)。在这种情况下,我们可以访问在合约中声明的owner
变量。但是,反过
来并不正确:你无法访问修饰函数中的任何变量。
# 合约继承
Solidity的合约对象支持 继承,这是一种用附加功能扩展基础合约的机制。要使用继承,请使用关键字is
指定父合约:
contract Child is Parent {
}
2
通过这个构造,Child
合约继承了Parent
的所有方法,功能和变量。Solidity还支持多重继承,可以在关键字is
之后用逗号分隔的合约
名称指定多重继承:
contract Child is Parent1, Parent2 {
}
2
合约继承使我们能够以实现模块化,可扩展性和重用的方式编写我们的合约。我们从简单的合约开始,实现最通用的功能,然后通过在更具体的合约中继承 这些功能来扩展它们。
在我们的Faucet
合约中,我们引入了构造函数和析构函数,以及为构建时指定的owner提供的访问控制。这些功能非常通用:许多合约都有它们。我们可以
将它们定义为通用合约,然后使用继承将它们扩展到Faucet
合约。
我们首先定义一个基础合约owned
,它拥有一个owner
变量,并在合约的构造函数中设置:
contract owned {
address owner;
// Contract constructor: set owner
constructor() {
owner = msg.sender;
}
// Access control modifier
modifier onlyOwner {
require(msg.sender == owner);
_;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
接下来,我们定义一个基本合约 mortal
,继承自 owned
:
contract mortal is owned {
// Contract destructor
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
2
3
4
5
6
如你所见,mortal
合约可以使用在owned
中定义的ownOwner
函数修饰器。它间接地也使用owner
address变量和owned
中定义的构造函数。继承
使每个合约变得更简单,并专注于其类的特定功能,使我们能够以模块化的方式管理细节。
现在我们可以进一步扩展owned
合约,在Faucet
中继承其功能:
contract Faucet is mortal {
// 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 {}
}
2
3
4
5
6
7
8
9
10
11
通过继承mortal
,继而继承owned
,Faucet
合约现在具有构造函数和销毁函数以及定义的owner。这些功能与Faucet
中的功能相同,但现在我们
可以在其他合约中重用这些功能而无需再次写入它们。代码重用和模块化使我们的代码更清晰,更易于阅读,并且更易于审计。
# 错误处理(assert, require, revert)
合约调用可以终止并返回错误。Solidity中的错误由四个函数处理:assert
, require
, revert
, 和 throw
(现在已弃用)。
当合约终止并出现错误时,如果有多个合约被调用,则所有状态变化(变量,余额等的变化)都会恢复,直至合约调用链的源头。这确保交易是原子的, 这意味着它们要么成功完成,要么对状态没有影响,并完全恢复。
assert
和require
函数以相同的方式运行,如果条件为假,则评估条件并停止执行并返回错误。按照惯例,当结果预期为真时使用assert
,
这意味着我们使用assert
来测试内部条件。相比之下,在测试输入(例如函数参数或交易字段)时使用require
,设置我们对这些条件的期望。
我们在函数修饰器onlyOwner
中使用了require
来测试消息发送者是合约的所有者:
require(msg.sender == owner);
require
函数充当_守护条件_,阻止执行函数的其余部分,并在不满足时产生错误。
从Solidity v.0.4.22开始,require
还可以包含有用的文本消息,可用于显示错误的原因。错误消息记录在交易日志中。所以我们可以通过在
我们的require
函数中添加一条错误消息来改进我们的代码:
require(msg.sender == owner, "Only the contract owner can call this function");
revert
和 throw
函数,停止执行合约并还原任何状态更改。throw
函数已过时,将在未来版本的Solidity中删除 - 你应该
使用revert
代替。revert
函数还可以将作为唯一参数的错误消息记录在交易日志中。
无论我们是否明确检查它们,合约中的某些条件都会产生错误。例如,在我们的Faucet
合约中,我们不检查是否有足够的ether来
满足提款请求。这是因为如果没有足够的余额进行转账,transfer
函数将失败并恢复交易:
The transfer function will fail if there is an insufficient balance
msg.sender.transfer(withdraw_amount);
但是,最好明确检查,并在失败时提供明确的错误消息。我们可以通过在转移之前添加一个require语句来实现这一点:
require(this.balance >= withdraw_amount,
"Insufficient balance in faucet for withdrawal request");
msg.sender.transfer(withdraw_amount);
2
3
像这样的其他错误检查代码会略微增加gas消耗,但它比不检查提供了更好的错误报告。在gas量和详细错误检查之间取得适当的平衡是你需要根据
合约的预期用途来决定的。在为测试网络设计的Faucet
的情况下,即使额外报告成本更高,我们也不冒险犯错。也许对于一个主网合约,我们会选择节约gas用量。
# 事件(Events)
事件是便于生产交易日志的Solidity构造。当一个交易完成(成功与否)时,它会产生一个 交易收据 transaction receipt,就像我们 在 [evm] 中看到的那样。交易收据包含_log_条目,用于提供有关在执行交易期间发生的操作的信息。事件是用于构造这些日志的Solidity高级对象。
事件在轻量级客户端和DApps中特别有用,它可以“监视”特定事件并将其报告给用户界面,或对应用程序的状态进行更改以反映底层合约中的事件。
事件对象接收序列化的参数并记录在区块链的交易日志中。你可以在参数之前应用关键字indexed
,以使其值作为索引表(哈希表)的一部分,可以由
应用程序搜索或过滤。
到目前为止,我们还没有在我们的Faucet
示例中添加任何事件,所以让我们来做。我们将添加两个事件,一个记录任何提款,一个记录任何存款。我们
将分别称这些事件Withdrawal
和Deposit
。首先,我们在Faucet
合约中定义事件:
contract Faucet is mortal {
event Withdrawal(address indexed to, uint amount);
event Deposit(address indexed from, uint amount);
[...]
}
2
3
4
5
6
我们选择将地址标记为indexed
,以允许任何访问我们的Faucet
的用户界面中搜索和过滤。
接下来,我们使用 emit
关键字将事件数据合并到交易日志中:
// Give out ether to anyone who asks
function withdraw(uint withdraw_amount) public {
[...]
msg.sender.transfer(withdraw_amount);
emit Withdrawal(msg.sender, withdraw_amount);
}
// Accept any incoming amount
function () public payable {
emit Deposit(msg.sender, msg.value);
}
2
3
4
5
6
7
8
9
10
Faucet.sol
合约现在看起来像:
Faucet8.sol: Revised Faucet contract, with events
include::code/Solidity/Faucet8.sol['code/Solidity/Faucet8.sol']
// Version of Solidity compiler this program was written for
pragma solidity ^0.4.22;
contract owned {
address owner;
// Contract constructor: set owner
constructor() {
owner = msg.sender;
}
// Access control modifier
modifier onlyOwner {
require(msg.sender == owner, "Only the contract owner can call this function");
_;
}
}
contract mortal is owned {
// Contract destructor
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
contract Faucet is mortal {
event Withdrawal(address indexed to, uint amount);
event Deposit(address indexed from, uint amount);
// Give out ether to anyone who asks
function withdraw(uint withdraw_amount) public {
// Limit withdrawal amount
require(withdraw_amount <= 0.1 ether);
require(this.balance >= withdraw_amount,
"Insufficient balance in faucet for withdrawal request");
// Send the amount to the address that requested it
msg.sender.transfer(withdraw_amount);
emit Withdrawal(msg.sender, withdraw_amount);
}
// Accept any incoming amount
function () public payable {
emit Deposit(msg.sender, msg.value);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 捕捉事件
好的,所以我们已经建立了我们的合约来发布事件。我们如何看到交易的结果并“捕捉”事件?web3.js
库提供一个数据结构,作为包含交易日志的交易的结果。
在那里,我们可以看到交易产生的事件。
让我们使用truffle
在修订的Faucet
合约上运行测试交易。按照 [truffle] 中的说明设置项目目录并编译Faucet
代码。源代码可以在本书的GitHub
存储库中找到:
code/truffle/FaucetEvents
[[testing_events_prep]]
$ truffle develop
truffle(develop)> compile
truffle(develop)> migrate
Using network 'develop'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0xb77ceae7c3f5afb7fbe3a6c5974d352aa844f53f955ee7d707ef6f3f8e6b4e61
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Faucet...
... 0xfa850d754314c3fb83f43ca1fa6ee20bc9652d891c00a2f63fd43ab5bfb0d781
Faucet: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...
truffle(develop)> Faucet.deployed().then(i => {FaucetDeployed = i})
truffle(develop)> FaucetDeployed.send(web3.toWei(1, "ether")).then(res => { console.log(res.logs[0].event, res.logs[0].args) })
Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } }
truffle(develop)> FaucetDeployed.withdraw(web3.toWei(0.1, "ether")).then(res => { console.log(res.logs[0].event, res.logs[0].args) })
Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
用deployed()
函数获得部署的合约后,我们执行两个交易。第一笔交易是一笔存款(使用send
),在交易日志中发出Deposit
事件:
Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } }
2
接下来,我们使用withdraw
函数进行提款。这会发出Withdrawal
事件:
Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }
2
为了获得这些事件,我们查看了作为结果(res
)返回的logs
数组。第一个日志条目(logs[0]
)包含logs[0].event
的事件名称和logs[0].args
的事件参数。通过在控制台上显示这些信息,我们可以看到发出的事件名称和事件参数。
事件是一种非常有用的机制,不仅适用于合约内通信,还适用于开发过程中的调试。
# 调用其他合约 (call, send, delegatecall, callcode)
在合约中调用其他合约是非常有用但有潜在危险的操作。我们将研究你可以实现的各种方法并评估每种方法的风险。
# 创建一个新的实例
调用另一份合约最安全的方法是你自己创建其他合约。这样,你就可以确定它的接口和行为。要做到这一点,你可以简单地使用关键字new
来实例化它,
就像任何面向对象的语言一样。在Solidity中,关键字new
将在区块链上创建合约并返回一个可用于引用它的对象。假设你想从另一个名为Token
的
合约中创建并调用Faucet
合约:
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = new Faucet();
}
}
2
3
4
5
6
7
这种合约建造机制确保你知道合约的确切类型及其接口。合约Faucet
必须在Token
范围内定义,如果定义位于另一个文件中,你可以使用import
语句来执行此操作:
import "Faucet.sol"
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = new Faucet();
}
}
2
3
4
5
6
7
8
9
new
关键字还可以接受可选参数来指定创建时传输的ether值
以及传递给新合约构造函数的参数(如果有):
import "Faucet.sol"
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = (new Faucet).value(0.5 ether)();
}
}
2
3
4
5
6
7
8
9
如果我们赋予创建的Faucet
一些ether,我们也可以调用Faucet
函数,它们就像方法调用一样操作。在这个例子中,我们从Token
的destroy
函数中调用Faucet
的destroy
函数:
import "Faucet.sol"
contract Token is mortal {
Faucet _faucet;
constructor() {
_faucet = (new Faucet).value(0.5 ether)();
}
function destroy() ownerOnly {
_faucet.destroy();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 访问现有的实例
我们可以用来调用合约的另一种方法是将现有合约的地址转换为实例。使用这种方法,我们将已知接口应用于现有实例。因此,我们需要确切地知道, 我们正在处理的事例实际上与我们所假设的类型相同,这一点非常重要。我们来看一个例子:
import "Faucet.sol"
contract Token is mortal {
Faucet _faucet;
constructor(address _f) {
_faucet = Faucet(_f);
_faucet.withdraw(0.1 ether)
}
}
2
3
4
5
6
7
8
9
10
11
在这里,我们将地址作为参数提供给构造函数,并将其作为Faucet
对象进行转换。这比以前的机制风险大得多,因为我们实际上并不知道该地址
是否实际上是Faucet
对象。当我们调用withdraw
时,我们假设它接受相同的参数并执行与我们的Faucet
声明相同的代码,但我们无法确定。
就我们所知,在这个地址的withdraw
函数可以执行与我们所期望的完全不同的事情,即使它的命名相同。因此,使用作为输入传递的地址并将它们
转换成特定的对象中比自己创建合约要危险得多。
# 原始调用, delegatecall
Solidity为调用其他合约提供了一些更“低级”的功能。它们直接对应于具有相同名称的EVM操作码,并允许我们手动构建合约到合约的调用。因此, 它们代表了调用其他合约最灵活和最危险的机制。
以下是使用 call
方法的相同示例:
contract Token is mortal {
constructor(address _faucet) {
_faucet.call("withdraw", 0.1 ether);
}
}
2
3
4
5
正如你所看到的,这种类型的call
,是一个函数的_盲_ _blind_调用,就像构建一个原始交易一样,只是在合约的上下文中。它可能会使我们的合约
面临一些安全风险,最重要的是 可重入性 reentrancy,我们将在 [reentrancy] 中更详细地讨论。如果出现问题,call
函数将返回false,
所以我们可以评估返回值以进行错误处理:
contract Token is mortal {
constructor(address _faucet) {
if !(_faucet.call("withdraw", 0.1 ether)) {
revert("Withdrawal from faucet failed");
}
}
}
2
3
4
5
6
7
call
的另一个变体是delegatecall
,它取代了更危险的callcode
。callcode
方法很快就会被弃用,所以不应该使用它。
正如[solidity_address_object]中提到的,delegatecall
不同于call
,因为msg
上下文不会改变。例如,call
将 msg.sender
的值
更改为发起调用的合约,而delegatecall
保持与发起调用的合约中的msg.sender
相同。基本上,delegatecall
在当前合约的上下文中运行另一
个合约的代码。它最常用于从library
调用代码。
应该谨慎使用delegatecall
。它可能会有一些意想不到的效果,特别是如果你调用的合约不是作为库设计的。
让我们使用示例合约来演示call
和delegatecall
用于调用库和合约的各种调用语义。我们使用一个事件来记录每个调用的来源,并根据调用类型
了解调用上下文如何变化:
CallExamples.sol: An example of different call semantics.
include::code/truffle/CallExamples/contracts/CallExamples.sol["code/truffle/CallExamples/contracts/CallExamples.sol"]
pragma solidity ^0.4.22;
contract calledContract {
event callEvent(address sender, address origin, address from);
function calledFunction() public {
emit callEvent(msg.sender, tx.origin, this);
}
}
library calledLibrary {
event callEvent(address sender, address origin, address from);
function calledFunction() public {
emit callEvent(msg.sender, tx.origin, this);
}
}
contract caller {
function make_calls(calledContract _calledContract) public {
// Calling the calledContract and calledLibrary directly
_calledContract.calledFunction();
calledLibrary.calledFunction();
// Low level calls using the address object for calledContract
require(address(_calledContract).call(bytes4(keccak256("calledFunction()"))));
require(address(_calledContract).delegatecall(bytes4(keccak256("calledFunction()"))));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
我们的主要合约是caller
,它调用库 calledLibrary
和合约 calledContract
。被调用的库和合约有相同的函数 calledFunction
,
发送calledEvent
事件。calledEvent
事件记录三个数据:msg.sender
, tx.origin
, 和 this
。每次调用calledFunction
时,
都会有不同的上下文(不同的 msg.sender
)取决于它是直接调用还是通过 delegatecall
调用。
在caller
中,我们首先直接调用合约和库的calledFunction()。然后,我们直接使用低级函数call
和delegatecall
调用
calledContract.calledFunction
。观察多种调用机制的行为。
让我们在truffle开发环境中运行并捕捉事件:
truffle(develop)> migrate
Using network 'develop'.
[...]
Saving artifacts...
truffle(develop)> web3.eth.accounts[0]
'0x627306090abab3a6e1400e9345bc60c78a8bef57'
truffle(develop)> caller.address
'0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f'
truffle(develop)> calledContract.address
'0x345ca3e014aaf5dca488057592ee47305d9b3e10'
truffle(develop)> calledLibrary.address
'0xf25186b5081ff5ce73482ad761db0eb0d25abfbf'
truffle(develop)> caller.deployed().then( i => { callerDeployed = i })
truffle(develop)> callerDeployed.make_calls(calledContract.address).then(res => { res.logs.forEach( log => { console.log(log.args) })})
{ sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' }
{ sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' }
{ sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' }
{ sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
让我们看看发生了什么。我们调用make_calls
函数并传递calledContract
的地址,然后捕获不同调用发出的四个事件。查看make_calls
函数,
让我们逐步了解每一步。
第一个调用:
_calledContract.calledFunction();
在这里,我们直接调用calledContract.calledFunction
,使用称为callFunction的高级ABI。发出的事件是:
sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10'
2
3
如你所见,msg.sender
是caller
合约的地址。tx.origin
是我们的钱包web3.eth.accounts[0]
的地址,钱包将交易发送给caller
。
该事件由calledContract
发出,我们从事件中的最后一个参数可以看到。
make_calls
中的下一次调用是对库的调用:
calledLibrary.calledFunction();
它看起来与我们调用合约的方式完全相同,但行为非常不同。我们来看看发出的第二个事件:
sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f'
2
3
这一次,msg.sender
不是caller
的地址。相反,它是我们钱包的地址,与交易来源相同。这是因为当你调用一个库时,这个调用
总是delegatecall
并且在调用者的上下文中运行。所以,当calledLibrary
代码运行时,它继承caller
的执行上下文,就好像
它的代码在caller
中运行一样。变量this
(在发出的事件中显示为from
)是caller
的地址,即使它是从calledLibrary
内部访问的。
接下来的两个调用,使用低级call
和delegatecall
,验证我们的期望,发出与我们刚刚看到的事件相同的结果。
# Gas 的考虑
Gas在 [gas] 一节中有更详细的描述,在智能合约编程中是一个非常重要的考虑因素。gas是限制以太坊允许交易消耗的最大计算量的资源。如果在 计算过程中超过了gas限制,则会发生以下一系列事件:
- 引发“out of gas”异常。
- 函数执行前的合约状态被恢复。
- 全部gas作为交易费用交给矿工,不予退还。
由于gas由创建该交易的用户支付,因此不鼓励用户调用gas成本高的函数。因此,程序员最大限度地减少合约函数的gas成本。为此,在构建智能 合约时建议采用某些做法,以尽量减少函数调用的gas成本。
# 避免动态大小的数组
函数中任何动态大小的数组循环,对每个元素执行操作或搜索特定元素会引入使用过多gas的风险。在找到所需结果之前,或在对每个元素采取行动之前, 合约可能会用尽gas。
# 避免调用其他合约
调用其他合约,尤其是在其函数的gas成本未知的情况下,会导致用尽gas的风险。避免使用未经过良好测试和广泛使用的库。 库从其他程序员收到的审查越少,使用它的风险就越大。
# 估算gas成本
例如,如果你需要根据调用参数估计执行某种合约函数所需的gas,则可以使用以下过程;
var contract = web3.eth.contract(abi).at(address);
var gasEstimate = contract.myAweSomeMethod.estimateGas(arg1, arg2, {from: account});
2
gasEstimate 会告诉我们执行需要的gas单位。
为了获得网络的 gas价格 可以使用:
var gasPrice = web3.eth.getGasPrice();
然后估算 gas成本
var gasCostInEther = web3.fromWei((gasEstimate * gasPrice), 'ether');
让我们应用我们的天然气估算函数来估计我们的Faucet
示例的天然气成本,使用此书代码库中的代码:
code/truffle/FaucetEvents
我们以开发模式启动truffle,并执行一个JavaScript文件gas_estimates.js
,其中包含:
gas_estimates.js: Using the estimateGas function
var FaucetContract = artifacts.require("./Faucet.sol");
FaucetContract.web3.eth.getGasPrice(function(error, result) {
var gasPrice = Number(result);
console.log("Gas Price is " + gasPrice + " wei"); // "10000000000000"
// Get the contract instance
FaucetContract.deployed().then(function(FaucetContractInstance) {
// Use the keyword 'estimateGas' after the function name to get the gas estimation for this particular function (aprove)
FaucetContractInstance.send(web3.toWei(1, "ether"));
return FaucetContractInstance.withdraw.estimateGas(web3.toWei(0.1, "ether"));
}).then(function(result) {
var gas = Number(result);
console.log("gas estimation = " + gas + " units");
console.log("gas cost estimation = " ` (gas * gasPrice) + " wei");
console.log("gas cost estimation = " + FaucetContract.web3.fromWei((gas * gasPrice), 'ether') + " ether");
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
truffle开发控制台显示:
$ truffle develop
truffle(develop)> exec gas_estimates.js
Using network 'develop'.
Gas Price is 20000000000 wei
gas estimation = 31397 units
gas cost estimation = 627940000000000 wei
gas cost estimation = 0.00062794 ether
2
3
4
5
6
7
8
9
建议你将函数的gas成本评估作为开发工作流程的一部分进行,以避免将合约部署到主网时出现意外。
# 安全考虑
在编写智能合约时,安全是最重要的考虑因素之一。与其他程序一样,智能合约将完全按写入的内容执行,这并不总是程序员所期望的。此外, 所有智能合约都是公开的,任何用户都可以通过创建交易来与他们进行交互。任何漏洞都可以被利用,损失几乎总是无法恢复。
在智能合约编程领域,错误代价高昂且容易被利用。因此,遵循最佳实践并使用经过良好测试的设计模式至关重要。
防御性编程 _Defensive programming_是一种编程风格,特别适用于智能合约编程,具有以下特点:
极简/简约
复杂性是安全的敌人。代码越简单,代码越少,发生错误或无法预料的效果的可能性就越小。当第一次参与智能合约编程时,
开发人员试图编写大量代码。相反,你应该仔细查看你的智能合约代码,并尝试找到更少的方法,使用更少的代码行,更少的复杂性和更少的“功能”。
如果有人告诉你他们的项目产生了“数千行代码”,那么你应该质疑该项目的安全性。更简单更安全。
代码重用
尽可能不要“重新发明轮子”。如果库或合约已经存在,可以满足你的大部分需求,请重新使用它。在你自己的代码中,遵循DRY原则:不要重复自己。
如果你看到任何代码片段重复多次,请问自己是否可以将其作为函数或库进行编写并重新使用。已被广泛使用和测试的代码可能比你编写的任何新代码更安全。
谨防“Not-Invented-Here”的态度,如果你试图通过从头开始构建“改进”某个功能或组件。安全风险通常大于改进值。
代码质量
智能合约代码是无情的。每个错误都可能导致经济损失。你不应该像通用编程一样对待智能合约编程。相反,你应该采用严谨的工程和软件开发方法论,
类似于航空航天工程或类似的不容乐观的工程学科。一旦你“启动”你的代码,你就无法解决任何问题。
可读性/可审核性
你的代码应易于理解和清晰。阅读越容易,审计越容易。智能合约是公开的,因为任何人都可以对字节码进行逆向工程。因此,你应该使用协作和开源方法
在公开场合开发你的工作。你应该编写文档良好,易于阅读的代码,遵循作为以太坊社区一部分的样式约定和命名约定。
测试覆盖
测试你可以测试的所有内容。智能合约运行在公共执行环境中,任何人都可以用他们想要的任何输入执行它们。你绝不应该假定输入(比如函数参数)是正确的,
并且有一个良性的目的。测试所有参数以确保它们在预期的范围内并且格式正确。
# 常见的安全风险
智能合约程序员应该熟悉许多最常见的安全风险,以便能够检测和避免使他们面临这些风险的编程模式。
# 重入 Re-entrancy
重入是编程中的一种现象,函数或程序被中断,然后在先前调用完成之前再次调用。在智能合约编程的情况下,当合约A调用合约B中的一个函数时, 可能会发生重入,合约B又调用合约A中的相同函数,导致递归执行。在合约状态在关键性调用结束之后才更新的情况下,这可能是特别危险的。
为了理解这一点,想象一下通过钱包合约调用银行合约的提现操作。合约A在合约B中调用提现功能,试图提取金额X。这种情况将涉及以下操作:
- 合约B检查A是否有必要的余额来提取X。
- B将X传送到A的地址(运行A的payable fallback函数)
- B更新A的余额以反映此次提现
无论何时向合约发送付款(如本例中),接收方合约(A)都有机会执行_payable_函数,例如默认的fallback函数。但是,恶意攻击者可以利用这种执行。 想象一下,在A的payable fallback中,合约A_再次_调用B银行的提款功能。B的提现功能现在将经历重入,因为现在相同的初始交易正在引发循环调用。
"(1) A 调用 B (2) B 调用 A 的 payable 函数 (1) A 再次调用 B "
在B的退出提现函数的第二次迭代中,B将再次检查A是否有可用余额。由于步骤3(其更新了A的余额)尚未执行,所以对于B来说,无论该函数被重新调用多少次, A仍然具有可用资金来提现。只要有gas可以继续运行,就可以重复该循环。当A检测到gas量不足时,它可以在payable函数中停止呼叫B. B将最终执行步骤3, 从A的余额中扣除X. 然而,这时,B可能已经执行了数百次转账,并且只扣除了一次费用。在这次袭击中,A有效地洗劫了B的资金。
这个漏洞因其与DAO攻击的相关性而特别出名。用户利用了这样一个事实,即在调用转移并提取价值数百万美元的ether后,合约中的余额才发生变化。
为了防止重入,最好的做法是让程序员使用_Checks-Effects-Interactions_模式,在进行调用之前应用函数调用的影响(例如减少余额)。在我们的例子中, 这意味着切换步骤3和2:在传输之前更新用户的余额。
在以太坊,这是完全没问题的,因为交易的所有影响都是原子的,这意味着在没有支付给用户的情况下更新余额是不可能的。要么都发生,要么抛出异常, 都不会发生。这样可以防止重入攻击,因为所有后续调用原始提现函数的操作都会遇到正确的修改后余额。通过切换这两个步骤,可以防止A的提现金额超过其余额。
# 设计模式
任何编程范式的软件开发人员通常都会遇到以行为,结构,交互和创建为主题的重复设计挑战。通常这些问题可以概括并重新应用于未来类似性质的问题。 当给定正式结构时,这些概括称为设计模式。智能合约有自己的一系列重复出现的设计问题,可以使用下面描述的一些模式来解决。
在智能合约的发展中存在着无数的设计问题,因此无法讨论所有这些问题 这里。因此,本节将重点讨论智能合约设计中最常见的三类问题分类:访问控制(access control),状态流(state flow)和资金支出(fund disbursement)。
在本节中,我们将制定一份合约,最终将包含所有这三种设计模式。该合约将运行投票系统,允许用户对“真相”进行投票。该合约将提出一项声明, 例如“小熊队赢得世界系列赛”。或者“纽约市正在下雨”,然后用户会有机会选择真或假。如果大多数参与者投票赞成"真"合约就认为该声明为真, 如果大多数参与者投票赞成“假”,则合约将认为该声明为“假”。为了激励真实性,每次投票必须向合约发送100 ether,而失败的少数派出的资金将分给大多数。 大多数参与者将从少数人中获得他们的部分奖金以及他们的初始投资。
这个“真相投票”系统实际上是Gnosis的基础,Gnosis是一个建立在以太坊之上的预测工具。有关Gnosis的更多信息,请访问:https://gnosis.pm/ (opens new window)
# 访问控制 Access control
访问控制限制哪些用户可以调用合约功能。例如,真相投票合约的所有者可能决定限制那些可以参与投票的人。 为了达到这个目标,合约必须施加两个访问限制:
- 只有合约的所有者可以将新用户添加到“允许的选民”列表中
- 只有允许的选民可以投票
Solidity函数修饰器提供了一个简洁的方式来实现这些限制。
_Note: 以下示例在修改器主体内使用下划线分号。这是Solidity的功能,用于告知编译器何时运行被修饰的函数的主体。 开发人员可以认为被修饰的函数的主体将被复制到下划线的位置。
pragma solidity ^0.4.21;
contract TruthVote {
address public owner = msg.sender;
address[] true_votes;
address[] false_votes;
mapping (address => bool) voters;
mapping (address => bool) hasVoted;
uint VOTE_COST = 100;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
modifier onlyVoter() {
require(voters[msg.sender] != false);
_;
}
modifier hasNotVoted() {
require(hasVoted[msg.sender] == false);
_;
}
function addVoter(address voter)
public
onlyOwner()
{
voters[voter] = true;
}
function vote(bool val)
public
payable
onlyVoter()
hasNotVoted()
{
if (msg.value >= VOTE_COST) {
if (val) {
true_votes.push(msg.sender);
} else {
false_votes.push(msg.sender);
}
hasVoted[msg.sender] = true;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
修饰器和函数的说明:
- onlyOwner: 这个修饰器可以修饰一个函数,使得函数只能被地址与owner相同的发送者调用。
- onlyVoter: 这个修饰器可以修饰一个函数,使得函数只能被已登记的选举人调用。
- addVoter(voter): 此函数用于将选民添加到选民列表。该功能使用onlyOwner修饰器,因此只有该合约的所有者可以调用它。
- vote(val): 这个函数被投票者用来对所提出的命题投下真或假。它用onlyVoter修饰器装饰,所以只有已登记的选民可以调用它。
# 状态流 State flow
许多合约将需要一些操作状态的概念。合约的状态将决定合约的行为方式以及在给定的时间点提供的操作。让我们回到我们的真实投票系统来获得更具体的例子。
我们投票系统的运作可以分为三个不同的状态。
. Register: 服务已创建,所有者现在可以添加选民。 . Vote: 所有选民投票。 . Disperse: 投票付款被分给大多数参与者。
以下代码继续建立在访问控制代码的基础上,但进一步将功能限制在特定状态。 在Solidity中,使用枚举值来表示状态是司空见惯的事情。
pragma solidity ^0.4.21;
contract TruthVote {
enum States {
REGISTER,
VOTE,
DISPERSE
}
address public owner = msg.sender;
uint voteCost;
address[] trueVotes;
address[] falseVotes;
mapping (address => bool) voters;
mapping (address => bool) hasVoted;
uint VOTE_COST = 100;
States state;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
modifier onlyVoter() {
require(voters[msg.sender] != false);
_;
}
modifier isCurrentState(States _stage) {
require(state == _stage);
_;
}
modifier hasNotVoted() {
require(hasVoted[msg.sender] == false);
_;
}
function startVote()
public
onlyOwner()
isCurrentState(States.REGISTER)
{
goToNextState();
}
function goToNextState() internal {
state = States(uint(state) + 1);
}
modifier pretransition() {
goToNextState();
_;
}
function addVoter(address voter)
public
onlyOwner()
isCurrentState(States.REGISTER)
{
voters[voter] = true;
}
function vote(bool val)
public
payable
isCurrentState(States.VOTE)
onlyVoter()
hasNotVoted()
{
if (msg.value >= VOTE_COST) {
if (val) {
trueVotes.push(msg.sender);
} else {
falseVotes.push(msg.sender);
}
hasVoted[msg.sender] = true;
}
}
function disperse(bool val)
public
onlyOwner()
isCurrentState(States.VOTE)
pretransition()
{
address[] memory winningGroup;
uint winningCompensation;
if (trueVotes.length > falseVotes.length) {
winningGroup = trueVotes;
winningCompensation = VOTE_COST + (VOTE_COST*falseVotes.length) / trueVotes.length;
} else if (trueVotes.length < falseVotes.length) {
winningGroup = falseVotes;
winningCompensation = VOTE_COST + (VOTE_COST*trueVotes.length) / falseVotes.length;
} else {
winningGroup = trueVotes;
winningCompensation = VOTE_COST;
for (uint i = 0; i < falseVotes.length; i++) {
falseVotes[i].transfer(winningCompensation);
}
}
for (uint j = 0; j < winningGroup.length; j++) {
winningGroup[j].transfer(winningCompensation);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
修饰器和函数的说明:
- isCurrentState: 在继续执行装饰函数之前,此修饰器将要求合约处于指定状态。
- pretransition: 在执行装饰函数的其余部分之前,此修饰器将转换到下一个状态
- goToNextState: 将合约转换到下一个状态的函数
- disperse: 计算大多数以及相应的瓜分奖金的功能。只有owner可以调用这个函数来正式结束投票。
- startVote: 所有者可用于开始投票的功能。
注意到允许所有者随意关闭投票流程可能会导致合约的滥用很重要。在更真实的实现中,投票期应在公众理解的时间段后结束。对于这个例子,这没问题。
现在增加的内容确保只有在owner决定开始投票阶段时才允许投票,用户只能在投票前由owner注册,并且在投票结束后才能分配资金。
# 提现 Withdraw
许多合约将为用户从中提取资金提供一些方法。在我们的示例中,属于大多数的用户在合约开始分配资金时直接接收资金。虽然这看起来有效, 但它是一种欠考虑的解决方案。在disperse中addr.send()调用的接收地址可以是一个合约,具有一个会失败的fallback函数,会打断disperse。 这有效地阻止了更多的参与者接收他们的收入。 一个更好的解决方案是提供一个用户可以调用来收取收入的提款功能。
...
enum States {
REGISTER,
VOTE,
DETERMINE,
WITHDRAW
}
mapping (address => bool) votes;
uint trueCount;
uint falseCount;
bool winner;
uint winningCompensation;
modifier posttransition() {
_;
goToNextState();
}
function vote(bool val)
public
onlyVoter()
isCurrentStage(State.VOTE)
{
if (votes[msg.sender] == address(0) && msg.value >= VOTE_COST) {
votes[msg.sender] = val;
if (val) {
trueCount++;
} else {
falseCount++;
}
}
}
function determine(bool val)
public
onlyOwner()
isCurrentState(State.VOTE)
pretransition()
posttransition()
{
if (trueCount > falseCount) {
winner = true;
winningCompensation = VOTE_COST + (VOTE_COST*false_votes.length) / true_votes.length;
} else if (falseCount > trueCount) {
winner = false;
winningCompensation = VOTE_COST + (VOTE_COST*true_votes.length) / false_votes.length;
} else {
winningCompensation = VOTE_COST;
}
}
function withdraw()
public
onlyVoter()
isCurrentState(State.WITHDRAW)
{
if (votes[msg.sender] != address(0)) {
if (votes[msg.sender] == winner) {
msg.sender.transfer(winningCompensation);
}
}
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
修饰器和(更新)功能的说明:
- posttransition: 函数调用后转换到下一个状态。
- determine: 此功能与以前的disperse功能非常相似,除了现在只计算赢家和获胜赔偿金额,实际上并未发送任何资金。
- vote: 投票现在被添加到votes mapping,并使用真/假计数器。
- withdraw: 允许投票者提取胜利果实(如果有)。
这样,如果发送失败,则只在一个特定的调用者上失败,不影响其他用户提取他们的胜利果实。
# 合约库
Github link: https://github.com/ethpm (opens new window)
Repository link: https://www.ethpm.com/registry (opens new window)
Website:https://www.ethpm.com/ (opens new window)
Documentation: https://www.ethpm.com/docs/integration-guide (opens new window)
# 安全最佳实践
Github:https://github.com/ConsenSys/smart-contract-best-practices/ (opens new window)
Docs:https://consensys.github.io/smart-contract-best-practices/ (opens new window)
也许最基本的软件安全原则是最大限度地重用可信代码。在区块链技术中,这甚至会凝结成一句格言:“Do not roll your own crypto”。就智能合约而言, 这意味着尽可能多地从经社区彻底审查的免费库中获益。
在Ethereum中,使用最广泛的解决方案是https://openzeppelin.org/[OpenZeppelin]套件,从ERC20和ERC721的Token实现,到众多众包模型, 到常见于“Ownable”,“Pausable”或“LimitBalance”等合约中的简单行为。该存储库中的合约已经过广泛的测试,并且在某些情况下甚至可以用作_de facto_标准实现。 它们可以免费使用,并且由https://zeppelin.solutions[Zeppelin]和不断增长的外部贡献者列表构建和修复。
同样来自Zeppelin的是https://zeppelinos.org/[zeppelin_os],一个用于安全地开发和管理智能合约应用程序的服务和工具的开源平台。 zeppelin_os在EVM之上提供了一个层,使开发人员可以轻松发布可升级的DApp,它们与经过良好测试的可自行升级的链上合约库链接。 这些库的不同版本可以共存于区块链中,凭证系统允许用户在不同方向上提出或推动改进。该平台还提供了一套用于调试,测试,部署和监控DApp的脱链工具。
# 进一步阅读
应用程序二进制接口(ABI)是强类型的,在编译时和静态时都是已知的。所有合约都有他们打算在编译时调用的任何合约的接口定义。
关于Ethereum ABI的更严格和更深入的解释可以在这找到:
https://solidity.readthedocs.io/en/develop/abi-spec.html
.
该链接包括有关编码的正式说明和各种有用示例的详细信息。
# 部署智能合约
//// TODO: add paragraph ////
# 测试智能合约
//// TODO: add paragraph ////
# 测试框架
有几个常用的测试框架(没有特定的顺序):
Truffle Test
Truffle框架的一部分,Truffle允许使用JavaScript(基于Mocha)或Solidity编写单元测试。这些测试是针对TestRPC/Ganache运行的。
编写这些测试的更多细节位于 [truffle]。
//// TODO: add anchor for [truffle] ////
Embark Framework Testing
Embark与Mocha集成,运行用JavaScript编写的单元测试。这些测试使用在TestRPC/Ganache上部署的合约执行。
Embark框架自动部署智能合约,并在合约被更改时自动重新部署它们。它还跟踪已部署的合约,并在真正需要时部署合约。Embark包括一个测试库,
它可以在EVM中快速运行和测试你的合约,并使用assert.equal()等函数。Embark测试将在目录测试下运行任何测试文件。
DApp
DApp使用本地Solidity代码(一个名为ds-test的库)和一个Parity构建的Rust库(称为Ethrun)执行以太坊字节码,然后断言正确性。
ds-test库提供用于验证控制台中数据记录的正确性和事件的断言功能。
断言函数包括
assert(bool condition)
assertEq(address a, address b)
assertEq(bytes32 a, bytes32 b)
assertEq(int a, int b)
assertEq(uint a, uint b)
assertEq0(bytes a, bytes b)
expectEventsExact(address target)
2
3
4
5
6
7
日志事件将信息记录到控制台,使其易于调试。
logs(bytes)
log_bytes32(bytes32)
log_named_bytes32(bytes32 key, bytes32 val)
log_named_address(bytes32 key, address val)
log_named_int(bytes32 key, int val)
log_named_uint(bytes32 key, uint val)
log_named_decimal_int(bytes32 key, int val, uint decimals)
log_named_decimal_uint(bytes32 key, uint val, uint decimals)
2
3
4
5
6
7
8
Populus
Populus使用python和自己的链仿真器来运行用Solidity编写的合约。单元测试是用pytest库编写的。Populus支持专门用于测试的书面合约。
这些合约文件名应该与glob模式 Test*.sol
匹配,并且位于项目测试目录./tests/
下的任何位置。
Framework | Test Language(s) | Testing Framework | Chain Emulator | Website |
Truffle | Javascript/Solidity | Mocha | TestRPC/Ganache | truffleframework.com |
Embark | Javascript | Mocha | TestRPC/Ganache | embark.readthedocs.io |
DApp | Solidity | ds-test (custom) | Ethrun (Parity) | dapp.readthedocs.io |
Populus | Python | Pytes | Python chain emulator | populus.readthedocs.io |
# 在区块链上测试
尽管大多数测试不应发生在部署的合约上,但可以通过以太坊客户端检查合约的行为。以下命令可用于评估智能合约的状态。这些命令应该在geth
终端输入,
尽管任何web3调用也会支持这些命令。
eth.getTransactionReceipt(txhash);
可用于获得在txhash
处的合约地址。
eth.getCode(contractaddress)
获取部署在contractaddress
的合约代码。这可以用来验证正确的部署。
eth.getPastLogs(options)
获取位于地址的合约的完整日志,在选项中指定。这有助于查看合约调用的历史记录。
eth.getStorageAt(address, position)
获取位于 address
的存储,并使用 position
的偏移量显示该合约中存储的数据。
# 第九章 开发工具,框架和库
以太坊智能合约开发变得轻松。自己做所有事情,你可以更好地理解所有事物如何结合在一起,但这是一项繁琐而重复的工作。 下面列出的框架可以自动执行某些任务并使开发变得轻而易举。
# Truffle
Github; https://github.com/Trufflesuite/Truffle (opens new window)
网站; https://Truffleframework.com (opens new window)
文档; https://Truffleframework.com/docs (opens new window)
Truffle Boxes; http://Truffleframework.com/boxes/ (opens new window)
npm
package repository; https://www.npmjs.com/package/Truffle (opens new window)
# 安装 Truffle 框架
Truffle 框架由多个 NodeJS
包组成。在我们安装Truffle
之前,我们需要安装NodeJS
和
Node Package Manager(npm
)的最新版。
推荐的安装 NodeJS
和 npm
的方法是使用node版本管理器(NVM)nvm
。一旦我们安装了nvm
,它将为我们处理所有依赖和更新。
我们将按照以下网址中的说明进行操作:
http://nvm.sh (opens new window)
一旦在你的操作系统上安装了nvm
,安装NodeJS
就很简单了。我们使用--lts
标志告诉nvm我们想要最新的“长期支持(LTS)”版本NodeJS
$ nvm install --lts
确认你已安装 node
和 npm
:
$ node -v
v8.9.4
$ npm -v
5.6.0
2
3
4
创建一个包含DApp支持的Node.js版本的隐藏文件.nvmrc,这样开发人员只需要在项目目录的根目录下运行nvm install
,它就会自动安装并切换到使用该版本。
$ node -v > .nvmrc
$ nvm install
2
看起来不错。现在安装Truffle:
$ npm -g install Truffle
+ Truffle@4.0.6
installed 1 package in 37.508s
2
3
4
# 集成预编译的 Truffle 项目 (Truffle Box)
如果我们想要使用或创建一个建立在预先构建的样板上的DApp,那么在Truffle Boxes链接中,我们可以选择一个现有的Truffle项目, 然后运行以下命令来下载并提取它:
$ Truffle unbox <BOX_NAME>
# 创建 Truffle 项目目录
对于我们将使用Truffle的每个项目,我们创建一个项目目录并在该目录中初始化Truffle。Truffle将在我们的项目目录中创建必要的目录结构。
通常,我们为项目目录指定一个描述我们项目的名称。对于这个例子,我们将使用Truffle从 [simple_contract_example] 部署我们的Faucet合约,
因此我们将命名项目文件夹Faucet
。
$ mkdir Faucet
$ cd Faucet
Faucet $
2
3
一旦进入 Faucet
目录,我们初始化 Truffle:
Faucet $ Truffle init
Truffle创建了一个目录结构和一些默认文件:
Faucet
├── contracts
│ └── Migrations.sol
├── migrations
│ └── 1_initial_migration.js
├── test
├── Truffle-config.js
└── Truffle.js
2
3
4
5
6
7
8
除了Truffle本身之外,我们还将使用一些JavaScript(nodeJS)支持包。我们可以用npm安装这些。我们初始化npm目录结构并接受npm建议的默认值:
$ npm init
package name: (faucet)
version: (1.0.0)
description:
entry point: (Truffle-config.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to Faucet/package.json:
{
"name": "faucet",
"version": "1.0.0",
"description": "",
"main": "Truffle-config.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Is this ok? (yes)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
现在,我们可以安装我们用来使Truffle变得更容易的依赖关系:
$ npm install dotenv Truffle-wallet-provider ethereumjs-wallet
你现在有一个带有数千个文件的node_modules
目录。
在将DApp部署到云生产或持续集成环境之前,指定 engines 字段非常重要,以便你的Dapp使用正确的Node.js版本进行构建,并且会安装相关的依赖关系。
Package.json“engines”字段配置链接; https://docs.npmjs.com/files/package.json#engines
# 配置 Truffle
Truffle 创建一些空的配置文件,Truffle.js
和Truffle-config.js
。在Windows系统上,当你尝试运行命令Truffle
,Windows尝试运行Truffle.js
时,Truffle.js
名称可能会导致冲突。为了避免这种情况,我们将删除Truffle.js
并使用Truffle-config.js
来支持Windows用户,他们的确已经受够了。
$ rm Truffle.js
现在我们编辑 Truffle-config.js 并用下面的内容替换:
Truffle-config.js - a Truffle configuration to get us started
module.exports = {
networks: {
localnode: { // Whatever network our local node connects to
network_id: "*", // Match any network id
host: "localhost",
port: 8545,
}
}
};
2
3
4
5
6
7
8
9
以上配置是一个很好的起点。它设置了一个默认的以太坊网络(名为localnode
),该网络假定你正在运行以太坊客户端(如Parity),既可以作为完整节点,
也可以作为轻客户端。该配置将指示Truffle与端口8545上的本地节点通过RPC进行通信。Truffle将使用本地节点连接的任何以太网(如Ethereum主网络)
或测试网络(如Ropsten)。本地节点也将提供钱包功能。
在下面的章节中,我们将配置其他网络供Truffle使用,比如ganache
test-RPC区块链和托管网络提供商Infura。随着我们添加更多网络,配置文件将变得
更加复杂,
但它也将为我们的测试和开发工作流程提供更多选择。
# 使用Truffle部署合约
我们现在有一个针对我们的 Faucet
项目的基本工作目录,并且我们已经配置了Truffle和它的依赖关系。合约在我们项目的contracts
子目录中。
该目录已经包含一个“helper”合约,Migrations.sol
管理合约升级。我们将在后面的章节中研究 Migrations.sol
的使用。
让我们将 Faucet.sol
合约([solidity_faucet_example])复制到contracts
子目录中,以便项目目录如下所示:
Faucet
├── contracts
│ ├── Faucet.sol
│ └── Migrations.sol
...
2
3
4
5
我们现在可以让Truffle编译我们的合约:
$ Truffle compile
Compiling ./contracts/Faucet.sol...
Compiling ./contracts/Migrations.sol...
Writing artifacts to ./build/contracts
2
3
4
# Truffle migrations - 理解部署脚本
Truffle提供了一个名为_migration_的部署系统。如果你曾在其他框架中工作过,你可能会看到类似的东西:Ruby on Rails,Python Django和
许多其他语言和框架都有migrate
命令。
在所有这些框架中,migration的目的是处理不同版本软件之间数据模式的变化。以太坊migration的目的略有不同。因为以太坊合约是不可变的,
而且要消耗gas部署,所以Truffle提供了一个migration机制来跟踪哪些合约(以及哪些版本)已经部署。在一个拥有数十个合约和复杂依赖关系的复杂项目中,
你不希望为了重新部署尚未更改的合约而支付gas。你也不想手动跟踪哪些合约的哪些版本已经部署了。Trufflemigration机制通过部署
智能合约Migrations.sol
完成所有这些工作,然后跟踪所有其他合约部署。
我们只有一份合约,Faucet.sol
,这至少意味着migration系统是大材小用的。不幸的是,我们必须使用它。但是,通过学习如何将它用于一个合约,
我们可以开始练习一些良好的开发工作习惯。随着事情变得更加复杂,这项努力将会得到回报。
Truffle的migrations
目录是找到迁移脚本的地方。现在,只有一个脚本 1_initial_migration.js
,它会部署 Migrations.sol
合约本身:
1_initial_migration.js - the migration script for Migrations.sol
include::code/truffle/Faucet/migrations/1_initial_migration.js
var Migrations = artifacts.require("./Migrations.sol");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};
2
3
4
5
6
7
我们需要第二个migration脚本来部署 Faucet.sol
。我们称之为2_deploy_contracts.js
。它非常简单,就像1_initial_migration.js
一样,
只需稍作修改即可。实际上,你可以复制1_initial_migration.js
的内容,并简单地将Migrations
的所有实例替换为Faucet
:
2_deploy_contracts.js - the migration script for Faucet.sol
include::code/truffle/Faucet/migrations/2_deploy_contracts.js
var Faucet = artifacts.require("./Faucet.sol");
module.exports = function(deployer) {
deployer.deploy(Faucet);
};
2
3
4
5
6
7
脚本初始化变量Faucet
,将Faucet.sol
Solidity源代码标识为定义Faucet
的工件。然后,它调用部署功能来部署此合约。
我们都准备好了。我们使用 truffle migrate
来部署合约。我们必须使用--network
参数指定在哪个网络上部署合约。我们只在配置文件中指定了一个网络,
我们将其命名为localnode
。确保你的本地以太坊客户端正在运行,然后输入:
Faucet $ Truffle migrate --network localnode
因为我们使用本地节点连接到以太坊网络并管理我们的钱包,所以我们必须授权Truffle创建的交易。我正在运行parity
连接到Ropsten测试区块链,
所以在Trufflemigration期间,我会在parity的Web控制台上看到一个弹出窗口:

Figure 1. Parity asking for confirmation to deploy Faucet
你将看到四笔交易,总计。一个部署
Migrations
,一个用于将部署计数器更新为1
,一个用于部署Faucet
,另一个用于将部署计数器更新为2
。 Truffle将显示migration完成情况,显示每个交易并显示合约地址:
$ Truffle migrate --network localnode
Using network 'localnode'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0xfa090db179d023d2abae543b4a21a1479e70ca7d35a469a5d1a98bfc6bd80fe8
Migrations: 0x8861c27715550bed8362c0345add158489df6db0
Saving successful migration to network...
... 0x985c4a32716826ddbe4eae284104bef8bc69e959899f62246a1b27c9dfcd6c03
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Faucet...
... 0xecdbeef77f0558edc689440e34b7bba0a3ba7a45e4b680b071b47c30a930e9d6
Faucet: 0xd01cd8e7bd29e4bff8c1693f59eee46137a9f300
Saving successful migration to network...
... 0x11f376bd7307edddfd40dc4a14c3f7cb84b6c921ac2465602060b67d08f9fd8a
Saving artifacts...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用Truffle控制台
Truffle提供了一个JavaScript控制台,我们可以使用这个控制台与Ethereum网络(通过本地节点)进行交互,与已部署的合约进行交互,
并与钱包提供商进行交互。在我们当前的配置(localnode
)中,节点和钱包提供商是我们的本地parity客户端。
让我们开始Truffle控制台并尝试一些命令:
$ Truffle console --network localnode
Truffle(localnode)>
2
Truffle提示一个提示符,显示所选的网络配置(localnode
)。记住并了解我们正在使用哪个网络很重要。你不希望在Ethereum主网络上意外部
署测试合约或进行交易。这可能是一个昂贵的错误!
Truffle控制台提供了自动补全功能,使我们可以轻松探索环境。如果我们在部分完成的命令后按Tab
,Truffle将为我们完成命令。如果有多个命令
与我们的输入相匹配,按Tab
两次将显示所有可能的完成。事实上,如果我们在空提示符下按两次Tab
,Truffle将列出所有命令:
Truffle(localnode)>
Array Boolean Date Error EvalError Function Infinity JSON Math NaN Number Object RangeError ReferenceError RegExp String SyntaxError TypeError URIError decodeURI decodeURIComponent encodeURI encodeURIComponent eval isFinite isNaN parseFloat parseInt undefined
ArrayBuffer Buffer DataView Faucet Float32Array Float64Array GLOBAL Int16Array Int32Array Int8Array Intl Map Migrations Promise Proxy Reflect Set StateManager Symbol Uint16Array Uint32Array Uint8Array Uint8ClampedArray WeakMap WeakSet WebAssembly XMLHttpRequest _ assert async_hooks buffer child_process clearImmediate clearInterval clearTimeout cluster console crypto dgram dns domain escape events fs global http http2 https module net os path perf_hooks process punycode querystring readline repl require root setImmediate setInterval setTimeout stream string_decoder tls tty unescape url util v8 vm web3 zlib
__defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__ __proto__ constructor hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf
2
3
4
5
6
绝大多数钱包和节点相关功能由web3
对象提供,该对象是 web3.js
库的一个实例。web3
对象将RPC接口抽象为我们的parity节点。你还会注意到
两个熟悉名称的对象:Migrations
和 Faucet
。这些代表我们刚刚部署的合约。我们将使用Truffle控制台与合约进行交互。首先,让我们通过 web3
对象检查我们的钱包:
Truffle(localnode)> web3.eth.accounts
[ '0x9e713963a92c02317a681b9bb3065a8249de124f',
'0xdb5dc1a13e3a55cf3b4587cd8d1e5fdeb6738145' ]
2
3
我们的parity客户端有两个钱包,Ropsten有一些测试ether。web3.eth.accounts
属性包含所有帐户的列表。我们可以使用 getBalance
函数检查第一个帐户的余额:
Truffle(localnode)> web3.eth.getBalance(web3.eth.accounts[0]).toNumber()
191198572800000000
Truffle(localnode)>
2
3
web3.js
是一个大型JavaScript库,通过提供者(如本地客户端)为以太坊系统提供全面的界面。我们将在[web3js]中更详细地研究web3.js
。
现在让我们尝试与我们的合约进行交互:
Truffle(localnode)> Faucet.address
'0xd01cd8e7bd29e4bff8c1693f59eee46137a9f300'
Truffle(localnode)> web3.eth.getBalance(Faucet.address).toNumber()
0
Truffle(localnode)>
2
3
4
5
接下来,我们将使用 sendTransaction
发送一些测试ether,为 Faucet
提供资金。请注意使用web3.toWei
为我们转换ether单位。
在没有出错的情况下输入十八个零既困难又危险,因此使用单位转换器来获取值总是更好。以下是我们发送交易的方式:
Truffle(localnode)> web3.eth.sendTransaction({from:web3.eth.accounts[0], to:Faucet.address, value:web3.toWei(0.5, 'ether')});
'0xf134c75b985dc0e0c27c2f0412251e0860eb530a5055e660f21e7483ab336808'
2
切换到parity
的Web控制台,你将看到一个弹出窗口,要求你确认该交易。等一下,一旦交易开始,你将能够看到我们的Faucet
合约的余额:
Truffle(localnode)> web3.eth.getBalance(Faucet.address).toNumber()
500000000000000000
2
我们调用withdraw
函数,从Faucet中取出一些测试ether:
Truffle(localnode)> Faucet.deployed().then(instance => {instance.withdraw(web3.toWei(0.1, 'ether'))}).then(console.log)
同样,你需要批准parity Web控制台中的交易。Faucet
的余额已经下降,我们的测试钱包已经收到0.1
ether:
Truffle(localnode)> web3.eth.getBalance(Faucet.address).toNumber()
400000000000000000
2
Truffle(localnode)> Faucet.deployed().then(instance => {instance.withdraw(web3.toWei(1, 'ether'))})
StatusError: Transaction: 0xe147ae9e3610334ada8d863c9028c12bd0501be2d0cfd05865c18612b92d3f9c exited with an error (status 0).
# Embark
Github; https://github.com/embark-framework/embark/ (opens new window)
文档; https://embark.status.im/docs/ (opens new window)
npm
package repository; https://www.npmjs.com/package/embark (opens new window)
$ npm -g install embark
# OpenZeppelin
https://openzeppelin.org/[OpenZeppelin] (opens new window) 是Solidity语言的一个可重复使用且安全的智能 合约的开放框架。
它由社区驱动,由https://zeppelin.solutions/[Zeppelin]团队领导,拥有超过一百名外部贡献者。该框架的主要重点是安全,通过应用行业标准合约 安全模式和最佳实践,利用zeppelin开发者从https://blog.zeppelin.solutions/tagged/security[auditing]中获得的所有经验大量的合约,并通过 社区的持续测试和审计,使用该框架作为其实际应用的基础。
OpenZeppelin框架是以太坊智能合约使用最广泛的解决方案。这是由于社区讨论每个提议的解决方案并从这些解决方案的实施和整合中学习,这将变成一个
不断增长的反馈循环,改进现有合约并找到将它们结合在一个清晰安全的体系结构中的新可能性。它不断增加新的可重用合约来解决越来越复杂的挑战,或在
以太坊区块链上探索激动人心的新可能性。如前所述,该框架目前拥有丰富的合约库,从ERC20和ERC721 token的实现,到众多的crowdsale模型,到简单的
行为,例如Ownable
,Pausable
或LimitBalance
。
在某些情况下,此存储库中的合约甚至作为_de facto_标准实现。
该框架拥有MIT许可证,并且所有合约都采用模块化方法进行设计,以确保易用性和扩展性。这些都是干净而基本的构建块,可以在你的下一个以太坊项目中使用。 让我们设置框架,并使用OpenZeppelin合约构建一个简单的crowdsale,作为使用它的简单例子。这个例子还强调了重用安全组件的重要性,而不是自己编写 安全组件,并给出了以太坊平台及其社区成为可能的想法的一个小样本。
首先,我们需要将npm中的openzepelin-solidity库安装到我们的工作区中。截至撰写本文时的最新版本是v1.9.0
,所以我们将使用该版本。
mkdir sample-crowdsale
cd sample-crowdsale
npm install openzeppelin-solidity@1.9.0
mkdir contracts
2
3
4
对于crowdsale而言,我们需要定义一个token,我们会给予投资者以换取其ether。在撰写本文时,OpenZeppelin包含了遵循ERC20,ERC721和ERC827 标准的多个基本token合约,具有不同的发行,限制,兑现,生命周期等特征。
让我们制作一个ERC20 token,这意味着初始供应从0开始,token所有者(在我们的例子中,是crowdsale合约)可以创建新的 token并分配给投资者。
为此,使用以下内容创建contracts/SampleToken.sol
文件:
include::code/OpenZeppelin/contracts/SampleToken.sol
pragma solidity 0.4.23;
import 'openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol';
contract SampleToken is MintableToken {
string public name = "SAMPLE TOKEN";
string public symbol = "SAM";
uint8 public decimals = 18;
}
2
3
4
5
6
7
8
9
10
11
OpenZeppelin已经提供了一个MintableToken合约,我们可以用它作为token的基础,所以我们只定义特定于我们案例的细节。你可以相信社区已经付出 很大的努力来确保合约的正确性,但最好自己核实一下。看看https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.9.0/contracts/token/ERC20/MintableToken.sol[MintableToken] (opens new window) ,https://github.com/OpenZeppelin的源代码/openzeppelin-solidity/blob/v1.9.0/contracts/token/ERC20/StandardToken.sol[StandardToken]和 https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.9.0/contracts/ownership/Ownable.sol[Ownable] 了解新token的实现细节及其支持的功能。另外,看看 https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/test/token/ERC20/MintableToken.test.js[automated tests] 以确保所有可能的场景都已经被覆盖,并且代码是安全的。
接下来,我们来制作Crowdsale(ICO)合约。就像 token一样,OpenZeppelin已经提供了各种各样的Crowdsale。目前,你将找到涉及分配、发行,价格和 验证的方案的合约。因此,假设你想为Crowdsale设定一个目标,并且如果在销售完成时没有达到目标,你想要退还所有投资者。为此,你可以 使用https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.9.0/contracts/crowdsale/distribution/RefundableCrowdsale.sol[RefundableCrowdsale]合约。 也许你想定义一个价格上涨的众筹来激励早期买家:https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.9.0/contracts/crowdsale/price/IncreasingPriceCrowdsale.sol[IncreasingPriceCrowdsale], 或者设定一个截止时间:https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.9.0/contracts/crowdsale/validation/TimedCrowdsale.sol[TimedCrowdsale], 或者设定购买者白名单:https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.9.0/contracts/crowdsale/validation/WhitelistedCrowdsale.sol[WhitelistedCrowdsale]。
正如我们之前所说的,OpenZeppelin合约是基本的构建块。这些crowdsale合约被设计为可组合的,
只需阅读 https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.9.0/contracts/crowdsale/Crowdsale.sol[Crowdsale]
合约的源代码即可了解关于如何扩展它的指导。
对于我们token的crowdsale,我们需要在crowdsale合约收到ether时才发行token,所以让我们使用
https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.9.0/contracts/crowdsale/emission/MintedCrowdsale.sol[MintedCrowdsale]
作为基础。为了让它更有趣,让我们把它做成 https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v1.9.0/contracts/crowdsale/distribution/PostDeliveryCrowdsale.sol[PostDeliveryCrowdsale],
token只能在众筹结束后赎回。将以下内容写入contracts/SampleCrowdsale.sol
:
include::code/OpenZeppelin/contracts/SampleCrowdsale.sol
pragma solidity 0.4.23;
import './SampleToken.sol';
import 'openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol';
import 'openzeppelin-solidity/contracts/crowdsale/distribution/PostDeliveryCrowdsale.sol';
contract SampleCrowdsale is PostDeliveryCrowdsale, MintedCrowdsale {
constructor(
uint256 _openingTime,
uint256 _closingTime
uint256 _rate,
address _wallet,
MintableToken _token
)
public
Crowdsale(_rate, _wallet, _token)
PostDeliveryCrowdsale(_openingTime, _closingTime)
{
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
同样,我们几乎不需要编写任何代码,只是为了重用OpenZeppelin社区为我们提供的已经经过测试的代码。但是,需要注意的是,
这种情况与我们的SampleToken
合约不同。如果你访问https://github.com/OpenZeppelin/openzeppelin-solidity/tree/v1.9.0/test/crowdsale[Crowdsale自动化测试],
你会看到它们被隔离测试。当你将不同的代码单元集成到一个更大的组件中时,单独测试所有的单元是不够的,因为它们之间的交互可能会导致你没有预料到的行为。
特别是,你会看到在这里我们介绍了多重继承,如果他们不了解Solidity编译器的实现细节,这可能会让开发人员感到意外。我们的SampleCrowdsale
非常简单,
并且能够像我们所期望的那样工作,因为框架旨在使这些案例变得简单明了;但不要因为这个框架引入的简单性,放松你的安全性。每当你集成部分OpenZeppelin
框架以构建更复杂的解决方案时,你必须全面测试解决方案的每个方面,以确保单元的所有交互按照你的意图运行。
最后,在我们对我们的解决方案感到满意并且我们已经彻底测试之后,我们需要部署它。OpenZeppelin与Truffle很好地集成在一起,所以我们可以 按照上面的Truffle部分所述编写一个migration文件。在 migrations/2_deploy_contracts.js 中写入以下内容:
include::code/OpenZeppelin/migrations/2_deploy_contracts.js
const SampleCrowdsale = artifacts.require('./SampleCrowdsale.sol');
const SampleToken = artifacts.require('./SampleToken.sol');
module.exports = function(deployer, network, accounts) {
const openingTime = web3.eth.getBlock('latest').timestamp + 2; // two secs in the future
const closingTime = openingTime + 86400 * 20; // 20 days
const rate = new web3.BigNumber(1000);
const wallet = accounts[1];
return deployer
.then(() => {
return deployer.deploy(SampleToken);
})
.then(() => {
return deployer.deploy(
SampleCrowdsale,
openingTime,
closingTime,
rate,
wallet,
SampleToken.address
);
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这只是OpenZeppelin框架中一些合约的简要概述。还有更多,社区总是提出新的想法,实施新的策略以使它们更安全,更简单,更清晰, 并尽早发现漏洞以防止主要网络合约中的漏洞。欢迎你加入社区进行学习和贡献。
Github link; https://github.com/OpenZeppelin/openzeppelin-solidity (opens new window)
Website link; https://openzeppelin.org/ (opens new window)
Docs link; https://openzeppelin.org/api/docs/open-zeppelin.html (opens new window)
# zeppelin_os
https://github.com/zeppelinos[zeppelin_os] (opens new window) 是一款开源的分布式工具和服务平台,位于EVM之上,安全地开发和管理智能合约应用程序。
与OpenZeppelin的代码每次都需要重新部署每个应用程序不同,zeppelin_os的代码处于链上。需要特定功能的应用程序(例如ERC20 token) 不仅不需要重新设计和重新审计其实施(OpenZeppelin解决了这些问题),而且甚至不需要部署它。使用zeppelin_os,应用程序可直接与 链上的token实现进行交互,这与桌面应用程序与其底层操作系统的组件进行交互的方式大致相同。
利用OpenZeppelin的应用程序通过重用库的组织和同行评审合约避免了“重新发明轮子”。然而,每当应用程序使用ERC20 token实现时,同一个ERC20字节码 会一次又一次地部署到区块链中。这种字节码在网络中无数次重复存在。现在,使用zeppelin_os的应用程序可以避免这种不必要的重复。他们并没有部署 自己的ERC20实施,而是链接到定义了社区接受的最新ERC20实现的合约。这种单一的中央实现仅部署在区块链中,与Solidity的库非常相似,但却相当复杂。
与Solidity的库不同,zeppelin_os提供的实现可以像常规合约一样对待,即它们具有存储空间。而且,它们是可升级的。如果在其中一个OS的官方实现中发现 了一个漏洞,它可以简单地与升级的漏洞交换。对于ERC20 token,对其实施的升级会立即波及到所有使用它的应用程序。操作系统不仅为其所有实现提供可升级性 ,还为用户自己的合约提供升级能力,甚至为其自己的代码库提供升级能力!开发人员决定应用程序何时以及如何实施升级,甚至决定要遵守哪种实施方案。
zeppelin_os的核心是一个非常聪明的合约,被称为“代理(proxy)”。代理是一种能够包装任何其他合约,暴露其接口而无需手动实现setter和getter的合约, 并且可以在不丢失状态的情况下进行升级。在Solidity术语中,它可以被看作是一个正常的合约,其业务逻辑包含在一个库中,随时可以由一个新库来交换,而不 会丢失其状态。代理链接到其实现的方式是完全自动的,并且封装给开发人员。实际上,任何合约都可以升级,代码几乎不变。关于zeppelin_os的代理机制的更 多信息可以在Zeppelin的博客中找到:https://blog.zeppelinos.org/[upgradeability-using-unstructured-storage] (opens new window),
操作系统将其实现封装在可使用ZepTokens担保(vouched)的软件包或“发行版”中。因此,可以针对某个版本押注ZepTokens,将其标识为社区可接受的实现集。 任何人都可以提交新的发行版,由社区审查并最终被接受为官方的新版本。作为奖励,发行版的开发者在它每次被押注时都会收到token。作为一名Dapp开发人员, 担保(vouching)提供了一种可测量的方式来确定给定发行版获得的支持,以及可信度。
使用Zeppelin_os开发应用程序与使用NPM开发Javascript应用程序类似。AppManager处理应用程序的每个版本的应用程序包。包只是一个合约目录,每个合约都 可以有一个或多个可升级的代理。AppManager不仅为特定于应用程序的合约提供代理,而且还以标准库的形式为Zeppelin_os实现提供代理。要查看这方面的完整 示例,请访问:https://github.com/zeppelinos/zos-lib/tree/master/[examples/complex] (opens new window)。
//// TODO: the example provided above is still a WIP - link to a tutorial once it's finished
虽然目前正在开发中,但zeppelin_os旨在提供一系列附加功能,例如开发人员工具,自动执行合约中后台操作的调度程序,开发奖励,促进应用程序之间进行通信和 交换价值的市场等等。 所有这些都在zeppelin_os的https://zeppelinos.org/zeppelin_os_[whitepaper].pdf (opens new window)中描述。
Github link; https://github.com/zeppelinos (opens new window) Website link; https://zeppelinos.org (opens new window) Blog: https://blog.zeppelinos.org (opens new window) Github: https://github.com/zeppelinos (opens new window)
# ethereumJS helpeth: 命令行实用程序
helpeth是一个命令行工具,使开发人员更容易的操作密钥和交易。
它是基于JavaScript的库和工具集合ethereumjs的一部分。
https://github.com/ethereumjs/helpeth (opens new window)
Usage: helpeth [command]
Commands:
signMessage <message> Sign a message
verifySig <hash> <sig> Verify signature
verifySigParams <hash> <r> <s> <v> Verify signature parameters
createTx <nonce> <to> <value> <data> Sign a transaction
<gasLimit> <gasPrice>
assembleTx <nonce> <to> <value> <data> Assemble a transaction from its
<gasLimit> <gasPrice> <v> <r> <s> components
parseTx <tx> Parse raw transaction
keyGenerate [format] [icapdirect] Generate new key
keyConvert Convert a key to V3 keystore format
keyDetails Print key details
bip32Details <path> Print key details for a given path
addressDetails <address> Print details about an address
unitConvert <value> <from> <to> Convert between Ethereum units
Options:
-p, --private Private key as a hex string [string]
--password Password for the private key [string]
--password-prompt Prompt for the private key password [boolean]
-k, --keyfile Encoded key file [string]
--show-private Show private key details [boolean]
--mnemonic Mnemonic for HD key derivation [string]
--version Show version number [boolean]
--help Show help [boolean]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# dapp.tools
https://dapp.tools/ (opens new window)
安装:
$ curl https://nixos.org/nix/install | sh
$ nix-channel --add https://nix.dapphub.com/pkgs/dapphub
$ nix-channel --update
$ nix-env -iA dapphub.{dapp,seth,hevm,evmdis}
2
3
4
# Dapp
https://dapp.tools/dapp/ (opens new window)
# Seth
https://dapp.tools/seth/ (opens new window)
# Hevm
https://dapp.tools/hevm/ (opens new window)
# SputnikVM
SputnikVM是一个独立的可插拔的用于不同的基于以太坊的区块链的虚拟机。它是用Rust编写的,可以用作二进制,货物箱,共享库,或者 通过FFI,Protobuf和JSON接口集成。它有一个单独的用于测试目的的二进制sputnikvm-dev,它模拟大部分JSON RPC API和区块挖掘。
Github link; https://github.com/etcdevteam/sputnikvm (opens new window)
# Libraries
# web3.js
web3.js是以太坊兼容的JS API,用于通过以太坊基金会开发的JSON RPC与客户进行通信。
Github link; https://github.com/ethereum/web3.js (opens new window)
npm
package repository link; https://www.npmjs.com/package/web3 (opens new window)
Documentation link for web3.js API 0.2x.x; https://github.com/ethereum/wiki/wiki/JavaScript-API (opens new window)
Documentation link for web3.js API 1.0.0-beta.xx; https://web3js.readthedocs.io/en/1.0/web3.html (opens new window)
# web3.py
web3.py 是一个用于与以太坊区块链进行交互的Python库。它现在也在以太坊基金会的GitHub中。
Github link; https://github.com/ethereum/web3.py (opens new window)
PyPi link; https://pypi.python.org/pypi/web3/4.0.0b9 (opens new window)
Documentation link; https://web3py.readthedocs.io/ (opens new window)
# EthereumJS
以太坊的库和实用程序集合。
Github link; https://github.com/ethereumjs (opens new window)
Website link; https://ethereumjs.github.io/ (opens new window)
# web3j
web3j 是Java和Android库,用于与Ethereum客户端集成并使用智能合约。
Github link; https://github.com/web3j/web3j (opens new window)
Website link; https://web3j.io (opens new window)
Documentation link; https://docs.web3j.io (opens new window)
# Etherjar
Etherjar 是与Ethereum集成并与智能合约一起工作的另一个Java库。它专为基于Java 8+的服务器端项目而设计,提供RPC的低层和高层封装, 以太坊数据结构和智能合约访问。
Github link; https://github.com/infinitape/etherjar (opens new window)
# Nethereum
Nethereum 是以太坊的.Net集成库。
Github link; https://github.com/Nethereum/Nethereum (opens new window)
Website link; http://nethereum.com/ (opens new window)
Documentation link; https://nethereum.readthedocs.io/en/latest/ (opens new window)
# ethers.js
ethers.js 库是一个紧凑,完整,功能齐全,经过广泛测试的以太坊库,完全根据MIT许可证获得,并且已收到来自以太坊基金会的DevEx资助以扩展和维护。
GitHub link: https://github.com/ethers-io/ethers.js (opens new window)
Documentation: https://docs.ethers.io (opens new window)
# Emerald Platform
Emerald Platform提供了库和UI组件,可以在以太坊上构建Dapps。Emerald JS和Emerald JS UI提供了一组模块和React组件来构建 Javascript应用程序和网站,Emerald SVG Icons是一组区块链相关的图标。除了Javascript库之外,它还有Rust库来操作私钥和交易签名。 所有Emerald库和组件都使用 Apache 2许可。
Github link; https://github.com/etcdevteam/emerald-platform (opens new window)
Documentation link; https://docs.etcdevteam.com (opens new window)
# 第十章 代币(Tokens)
# Tokens
# 什么是Token?
单词_Token_来源于古英语“tacen”,意思是符号或符号。常用来表示私人发行的类似硬币的物品,价值不大,例如交通Token,洗衣Token,游乐场Token。
如今,基于区块链的Token将这个词重新定义为基于区块链的抽象概念,可以被拥有,并代表资产,货币或访问权。
“Token”一词与微不足道的价值之间的联系与物理Token的使用限制有很大关系。通常仅限于特定的企业,组织或地点,物理Token不易交换,不能用于多个功能。 通过区块链标记,这些限制被删除。这些Token中的许多Token在全球范围内有多种用途,可以在全球流动市场中相互交易或与其他货币交易。随着这些限制的消失, “微不足道的价值”的期望也成为过去。
在本节中,我们将看到Token的各种用法以及它们的创建方式。我们还讨论Token的属性,如可互换性和内在性等。最后,我们通过基于构建自己的Token的实验来检验它们的标准和技术。
# 如何使用Token?
Token最明显的用途是作为数字私人货币。但是,这只是一个可能的用途。Token可以被编程为提供许多不同的功能,通常是重叠的。例如,Token可以同时传达投票权, 访问权和资源所有权。货币只是第一个“应用程序”。
货币
Token可以作为一种货币形式,其价值通过私人交易来确定。例如,ether或bitcoin。
资源
Token可以表示在共享经济或资源共享环境中获得或生成的资源。例如,表示可通过网络共享的资源的存储或CPU的Token。
资产
Token可以代表内在或外在,有形或无形资产的所有权。例如,黄金,房地产,汽车,石油,能源等
访问
Token可以代表访问权限,可以访问数字或实体资产,例如论坛,专属网站,酒店房间,租车。
权益
Token可以代表数字组织(例如DAO)或法律虚拟主体(例如公司)中的股东权益
投票
Token可以代表数字或法律系统中的投票权。
收藏品
Token可以代表数字(例如CryptoPunks)或物理收藏品(例如绘画)
身份
Token可以代表数字(例如头像)或合法身份(例如国家ID)。
证明
Token可以代表某些机构或去中心化的信用系统(例如婚姻记录,出生证明,大学学位)的认证或事实证明。
实际用途 Token可用于访问或支付服务。
通常,单个Token包含其中几个功能。有时它们之间很难辨别,因为物理等价物一直是密不可分的。例如,在物理世界中,驾驶执照(认证)也是身份证件(身份证明), 两者不能分开。在数字领域,以前的混合功能可以独立分离和开发(例如匿名认证)。
# Tokens和可互换性
来自Wikipedia:
在经济学中,可互换性是一种商品或商品的财产,其独立单位本质上是可以互换的
。
当我们可以将Token的任何单个单元替换为另一个Token而其价值或功能没有任何差异时,Token是可替代的。例如,ether是一种 可替代的Token,因为任何ether的单位具有相同的值并且与任何其他单位的ether一起使用。
严格地说,如果可以跟踪Token的历史出处,那么它就不是完全可替代的。追踪出处的能力可能导致黑名单和白名单,从而降低或 消除互通性。我们将在[privacy]中进一步研究。
不可互换的Token是代表独特的有形或无形商品的Token,因此不可互换。例如,表示_特定的_ Van Gogh绘画所有权的Token不等 同于代表毕加索的另一个Token。同样,表示特定数字收藏的Token,如特定的CryptoKitty(请参阅[cryptoKitties])与任何 其他CryptoKitty都不可互换。每个不可互换的Token与唯一标识符相关联,例如序列号。
本节后面我们将看到可替换和不可替换Token的例子。
# 交易对手风险
交易对手风险是交易中的其他方不能履行其义务的风险。由于在交易中增加了两个以上的交易方,某些类型的交易会产生额外的交易 对手风险。例如,如果你持有贵金属存款证明并将其出售给某人,则该交易中至少有三方:卖方,买方和贵金属的保管人。有人持有 有形资产,必要时他们成为一方并为涉及该资产的任何交易添加交易对手风险。当资产通过交换所有权信息而间接交易时,资产托管 人有额外的交易对手风险。他们有资产吗?他们是否会根据Token的转让(例如证书,契约,所有权或数字Token)识别(或允许) 所有权的转移?在数字Token的世界中,了解谁持有由Token表示的资产以及适用于该基础资产的规则很重要。
# Tokens和内在性
单词 "intrinsic" 源于拉丁词 "intra", 表示"来自内部".
一些Token代表对区块链来说是 内在的 的数字项目。这些数字资产受共识规则的约束,就像Token本身一样。这具有重要的意义: 代表固有资产的Token不会带来额外的交易对手风险。如果你持有1 ether的密钥,就没有其他方为你持有那个以太。区块链共识规 则的应用,使得你对私钥的所有权(控制权)等同于资产的所有权,无需任何中介。
相反,许多Token用于表示 外在的 extrinsic 事物,如房地产,公司投票股票,商标,金条。这些项目的所有权不属于区块链, 属于法律,习惯和政策,与管理Token的共识规则分开。换句话说,Token发行人和所有者可能仍然依赖现实世界的非智能合约。因此, 这些外部资产会带来额外的交易对手风险,因为它们由托管人持有,记录在外部注册管理机构中,或由区块链环境以外的法律和政策控制。
基于区块链的Token最重要的后果之一是能够将外部资产转换为内部资产,从而消除交易对手风险。一个很好的例子就是从一家公司的股权 (外部)转向一个 去中心化的自治组织 或类似的(内部)组织的股权或投票权。
# 使用Tokens:效用或权益
今天以太坊的几乎所有项目都以某种形式发布。但是,所有这些项目真的需要一个Token吗?使用Token有什么缺点,或者我们会看到口号 “将所有东西Token化”的口号是否成熟?
首先,让我们首先澄清Token在新项目中的作用。大多数项目都以两种方式之一使用Token:要么是“实用Token”,要么是“权益Token”。 很多时候,这两个角色是混合在一起的,难以区分。
实用Token是那些需要使用Token来支付服务,应用程序或资源的Token。实用Token的例子包括代表资源的Token,如共享存储,访问社 交媒体网络等服务,或将ether作为以太坊平台的gas。相比之下,权益Token是代表创业公司股票的Token。
股权Token可以像享有利润和分红的无投票权股份一样有限,或者向去中心化自治组织的有投票权的股票一样广泛,其中平台是通过Token 持有者的多数投票管理的。
# 它不是一只鸭子
仅仅因为Token用于为初创公司筹款,并不意味着它必须用作服务的支付,反之亦然。然而,许多初创公司面临着一个难题:Token是一种 很好的筹款机制,但向大众提供证券(股权)是大多数司法管辖区的受监管活动。通过将股权Token伪装成实用Token,许多创业公司希望 能够绕过这些监管限制,并从公开募股筹集资金,同时将其作为预售的实用Token。这些稀薄的股权产品是否能够摆脱监管机构仍有待观察。
正如俗语所说:“如果它像鸭子一样走路,像鸭子一样嘎嘎叫 - 它就是一只鸭子。” 监管机构不会因这些语义扭曲而分心,恰恰相反,他们 更有可能将这种法律诡辩看作是企图欺骗公众。
[[who_needs_utility_Tokens]]
# 实用Token:谁需要它们?
真正的问题是实用Token为初创公司带来了重大风险和被采用障碍。也许在遥远的将来,“将所有事物Token化”成为现实。但是,目前,获得 ,理解和使用Token的人数是已经很小的加密货币市场的一个子集。
对于创业公司而言,每项创新都代表着风险和市场过滤。创新走的是人迹罕至的路,远离传统的道路。它已经是一个孤独的散步。如果一家创 业公司试图在一个新的技术领域进行创新,比如P2P网络上的存储共享,那么这是一条足够孤单的道路。为该创新添加实用Token并要求用户 采用Token以使用该服务会增加风险并增加被采用的障碍。它走出了已然孤独的P2P存储创新之路,进入荒野。
将每项创新视为过滤器。它限制了可以成为这种创新的早期采用者的市场子集。添加第二个过滤器化合物,会进一步限制可找到的市场。你要 求你的早期采用者采用的不仅仅是两种全新的技术:你构建的新颖应用程序/平台/服务以及Token经济。
对于初创公司而言,每项创新都会带来风险,从而增加创业失败的可能性。如果你已经采取冒险的创业想法并添加实用Token,则也同时增加 了所有底层平台(以太坊),更广泛的经济(交易所,流动性),监管环境(股票/商品监管机构)和技术(智能合约,Token标准)的风险。 这对创业公司来说是一个很大的风险。
“Tokenize all the things”的倡导者可能会通过采用Token来反对上述说法,他们也继承了整个Token经济的市场热情,早期采用者,技 术,创新和流动性。这也是事实。问题是收益和热情是否超过风险和不确定性。
尽管如此,一些最具创新性的商业理念确实发生在加密领域。如果监管机构不能快速通过法律并支持新的商业模式,人才和企业家将寻求在更 加加密友好的其他司法辖区开展业务。这实际上正在发生。
最后,在本章开始时,介绍Token时,我们将Token的口语意义解释为“微不足道的价值”。大多数Token的价值微不足道的根本原因是因为它 们只能用在非常狭窄的环境中:一家巴士公司,一家洗衣店,一家商场,一家酒店,一家公司商店。流动性有限,适用性有限,转换成本高, 一路降低Token的价值,直到它只有“Token”那么小的价值。因此,当你将实用Token添加到你的平台上,但该Token只能在你自己的一个平台 上使用且市场很小时,则会重新创建使物理Token毫无价值的条件。如果为了使用你的平台,用户必须将某些东西转换为你的实用Token,使 用它,然后将其余部分再转换回更普遍有用的东西,你实际是创建了公司凭证。数字Token的转换成本比没有市场的物理Token低了几个数量 级,但转换成本并不是零。在整个行业中工作的实用Token将非常有趣并且可能非常有价值。但是,如果你将创业公司设定为必须引导整个行 业标准才能成功,那么你可能已经失败了。
在像以太坊这样的通用平台上部署服务的好处之一就是能够连接智能合约,增加流动性和Token效用的潜力。
为了正确的理由做出这个决定。采用Token是因为你的应用程序_不使用Token无法工作_ (例如以太坊)。采用它是因为Token解决了基本的 市场障碍或访问问题。不要因为这是你可以快速筹集资金的唯一方式而引入实用Token,你需要假装它不是公开发行的证券。
# Token 标准
区块链标记在以太坊之前就已存在。在某些方面,第一块区块链货币比特币本身就是一种Token。在Ethereum之前,还在比特币和其他加密货 币上开发了许多Token平台。然而,在以太坊上引入第一个Token标准导致Token爆炸。
Vitalik Buterin建议将Token作为通用可编程区块链(如以太坊)最明显和最有用的应用之一。事实上,在以太坊的第一年,经常看到 Vitalik和其他人穿着印有Ethereum标志的T恤和背面的智能合约样本。这件T恤有几种变化,但最常见的是一种Token的实现。
# ERC20 Token 标准
第一个标准由Fabian Vogelsteller于2015年11月引入,作为以太坊征求意见(ERC)。它被自动分配了GitHub发行号码20,从而获得了名 字“ERC20 Token”。绝大多数Token目前都基于ERC20。ERC20征求意见最终成为以太坊改进建议EIP20,但大多仍以原名ERC20提及。你可以 在这里阅读标准:
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md (opens new window)
ERC20是_可替换Token_的标准,意味着ERC20标记的不同单元是可互换的,没有唯一属性。
ERC20标准为实现Token的合同定义了一个通用接口,这样任何兼容的Token都可以以相同的方式访问和使用。该接口包含许多必须存在于标准 的每个实现 中的函数,以及可能由开发人员添加的一些可选函数和属性。
# ERC20 必须的函数和事件
totalSupply
返回当前存在的Token的总单位。ERC20Token可以有固定或可变的供应量。
balanceOf
给定一个地址,返回该地址的Token余额。
transfer
给定一个地址和数量,将该数量的Tokens从执行该方法的地址的余额转移到该地址。
transferFrom
给定发送者,接收人和数量,将Token从一个帐户转移到另一个帐户。与+approve+结合使用。
approve
在给定接收者地址和数量的情况下,授权该地址从发布批准的帐户执行多次转账,直到到达指定的数量。
allowance
给定一个所有者地址和一个消费者地址,返回该消费者被批准从所有者的取款的剩余金额。
Transfer event
成功转移时触发的事件(调用+transfer+或+transferFrom+)(即使对于零值转移)。
Approval event
成功调用+approve+时记录的事件。
# ERC20 可选函数
name
返回Token的可读名称(例如“US Dollars”)。
symbol
返回Token的人类可读符号(例如“USD”)。
decimals
返回用于分割Token数量的小数位数。例如,如果小数为2,则将Token数除以100以获取其用户表示。
# 在Solidity中定义的ERC20接口
以下是在Solidity中ERC20接口规范的样子:
contract ERC20 {
function totalSupply() constant returns (uint theTotalSupply);
function balanceOf(address _owner) constant returns (uint balance);
function transfer(address _to, uint _value) returns (bool success);
function transferFrom(address _from, address _to, uint _value) returns (bool success);
function approve(address _spender, uint _value) returns (bool success);
function allowance(address _owner, address _spender) constant returns (uint remaining);
event Transfer(address indexed _from, address indexed _to, uint _value);
event Approval(address indexed _owner, address indexed _spender, uint _value);
}
2
3
4
5
6
7
8
9
10
# ERC20 数据结构
如果你检查任何ERC20实现,它将包含两个数据结构,一个用于追踪余额,另一个用于追踪配额(allowances)。在Solidity中,它们使用 data mapping 实现。
第一个data mapping按拥有者实现了Token余额的内部表。这允许Token合约跟踪谁拥有Token。每次转账都是从一个余额中扣除的,并且是 对另一个余额的增加。
Balances: a mapping from address (owner) to amount (balance)
mapping(address => uint256) balances;
第二个数据结构是配额的data mapping。正如我们将在[transfer_workflows]中看到的那样,使用ERC20Token,Token的所有者可以将权 限委托给花钱者,允许他们从所有者的余额中花费特定金额(配额)。ERC20合同通过二维映射追踪配额,主关键字是Token所有者的地址,映射到一个花费者地址和配额金额:
Allowances: a mapping from address (owner) to address (spender) to amount (allowance)
mapping (address => mapping (address => uint256)) public allowed;
# ERC20工作流程:“transfer”和“approve & transferFrom”
ERC20Token标准具有两种传输功能。你可能想知道为什么?
ERC20允许两种不同的工作流程。第一个是使用transfer
函数的单次交易,简单的工作流程。这个工作流程是钱包用来将Token发送给其他
钱包的工作流程。绝大多数Token交易都发生在transfer
工作流程中。
执行转让合同非常简单。如果爱丽丝希望向鲍勃发送10个Token,她的钱包会向Token合约的地址发送一个交易,并用Bob的地址和“10”作为
参数调用transfer
函数。Token合约调整Alice的余额(-10)和Bob的余额(+10)并发出Transfer
事件。
第二个工作流程是使用approve
和transferFrom
的双交易工作流程。该工作流程允许Token所有者将其控制权委托给另一个地址。它通
常用于委托控制权给合约来分配Token,但它也可以被交易所使用。例如,如果一家公司为ICO出售Token,他们可以approve
一个
crowdsale合同地址来分发一定数量的Token。然后crowdsale合同可以用transferFrom
转移给Token的每个买家。

Figure 1. The two-step approve & transferFrom workflow of ERC20 Tokens
对于
approve & transferFrom
工作流程,需要两个交易。假设Alice希望允许AliceICO合同将所有AliceCoin Token的50%卖给像Bob
和Charlie这样的买方。首先,Alice发布AliceCoin ERC20合同,将所有AliceCoin发放到她自己的地址。然后,Alice发布可以以ether
出售Token的AliceICO合同。接下来,Alice启动approve & transferFrom
工作流程。她向AliceCoin发送一个交易,调用approve
,
参数是AliceICO的地址和totalSupply
的50%。这将触发Approval
事件。现在,AliceICO合同可以出售AliceCoin了。 当AliceICO从Bob那收到ether,它需要发送一些AliceCoin给Bob作为回报。在AliceICO合约内是AliceCoin和ether之间的汇率。Alice
在创建AliceICO时设置的汇率决定了Bob将根据他发送给AliceICO的ether数量能得到多少Token。当AliceICO调用AliceCoin
transferFrom
函数时,它将Alice的地址设置为发送者,将Bob的地址设置为接收者,并使用汇率来确定将在“value”字段中将多少
AliceCoin Token传送给Bob。AliceCoin合同将余额从Alice的地址转移到Bob的地址并触发 Transfer
事件。只要不超过Alice设定的
审批限制,AliceICO合同可以调用 transferFrom
无限次数。AliceICO合同可以通过调用allowance
函数来跟踪它能卖出多少
AliceCoinToken。
# ERC20 实现
虽然可以在约三十行Solidity代码中实现兼容ERC20的Token,但大多数实现都更加复杂,以解决潜在的安全漏洞。在EIP20标准中提到了两 种实现:
Consensys EIP20:: 简单易读的ERC20兼容Token的实现。
你可以在此处阅读Consensys实现的Solidity代码: https://github.com/ConsenSys/Tokens/blob/master/contracts/eip20/EIP20.sol (opens new window)
OpenZeppelin StandardToken:: 此实现与ERC20兼容,并具有额外的安全防范措施。它构成了OpenZeppelin库的基础,实现了更复杂的 与ERC20兼容的Token,包括筹款上限,拍卖,归属时间表和其他功能。
你可以在这里看到OpenZeppelin StandardToken的Solidity代码:
https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/Token/ERC20/StandardToken.sol (opens new window)
# 发布我们自己的ERC20Token
让我们创建并发布我们自己的Token。在这个例子中,我们将使用truffle
框架(参见[truffle])。该示例假设你已经安装了
truffle
,进行了配置,并熟悉其基本操作。
我们将称之为“Mastering Ethereum Token”,标志为“MET”。
你可以在本书的GitHub仓库中找到这个例子: https://github.com/ethereumbook/ethereumbook/blob/develop/code/truffle/METoken (opens new window)
首先,让我们创建并初始化一个Truffle项目目录,就像我们在[truffle_project_directory]中所做的那样。运行这四个命令并接受任何 问题的默认答案:
$ mkdir METoken
$ cd METoken
METoken $ truffle init
METoken $ npm init
2
3
4
你现在应该具有以下目录结构:
METoken/
├── contracts
│ └── Migrations.sol
├── migrations
│ └── 1_initial_migration.js
├── package.json
├── test
├── truffle-config.js
└── truffle.js
2
3
4
5
6
7
8
9
编辑truffle.js
或truffle-config.js
配置文件以设置Truffle
环境,或复制我们使用的环境:
如果使用示例truffle-config.js
,请记住在包含你的测试私钥的METoken
文件夹中创建文件.env
,以便在公共以太网测试网络
(如ganache或Kovan)上进行测试和部署。你可以从MetaMask中导出你的测试网络私钥。
之后你的目录看起来像:
METoken/
├── contracts
│ └── Migrations.sol
├── migrations
│ └── 1_initial_migration.js
├── package.json
├── test
├── truffle-config.js
├── truffle.js
└── .env *new file*
2
3
4
5
6
7
8
9
10
WARNING
只能使用不用于在以太坊主网络上持有资金的测试密钥或测试助记符。切勿使用持有真正金钱的钥匙进行测试。
对于我们的示例,我们将导入OpenZeppelin StandardContract,它实现了一些重要的安全检查并且易于扩展。让我们导入该库:
$ npm install zeppelin-solidity
+ zeppelin-solidity@1.6.0
added 8 packages in 2.504s
2
3
4
zeppelin-solidity
包将在node_modules
目录下添加约250个文件。OpenZeppelin库包含的不仅仅是ERC20Token,但我们只使
用它的一小部分。
接下来,让我们编写我们的Token合约。创建一个新文件METoken.sol
并从GitHub复制示例代码:
我们的合同非常简单,因为它继承了OpenZeppelin StandardToken库的所有功能:
METoken.sol : A Solidity contract implementing an ERC20 Token
include::code/METoken/contracts/METoken.sol[]
在这里,我们定义可选变量name
,symbol
和decimals
。我们还定义了一个_initial_supply
变量,设置为2,100万个Token,以及
两个小数细分(总共21亿)。在契约的初始化(构造函数)函数中,我们将totalSupply
设置为等于_initial_supply
,并将所有
_initial_supply
分配给创建 METoken
契约的帐户余额(msg.sender
)。
我们现在使用truffle
编译METoken
代码:
$ truffle compile
Compiling ./contracts/METoken.sol...
Compiling ./contracts/Migrations.sol...
Compiling zeppelin-solidity/contracts/math/SafeMath.sol...
Compiling zeppelin-solidity/contracts/Token/ERC20/BasicToken.sol...
Compiling zeppelin-solidity/contracts/Token/ERC20/ERC20.sol...
Compiling zeppelin-solidity/contracts/Token/ERC20/ERC20Basic.sol...
Compiling zeppelin-solidity/contracts/Token/ERC20/StandardToken.sol...
2
3
4
5
6
7
8
9
如你所见,truffle
包含了OpenZeppelin库的必要依赖关系,并编译了这些契约。
我们建立一个migration脚本,部署 METoken
合约。在METoken/migrations
文件夹中创建一个新文件2_deploy_contracts.js
。
从Github存储库中的示例复制内容:
以下是它包含的内容:
2_deploy_contracts: Migration to deploy METoken
include::code/METoken/migrations/2_deploy_contracts.js[]
var METoken = artifacts.require("METoken");
module.exports = function(deployer) {
// Deploy the METoken contract as our only task
deployer.deploy(METoken);
};
2
3
4
5
6
7
8
在我们部署其中一个以太坊测试网络之前,让我们开始一个本地区块链来测试一切。正如我们在[using_ganache]中所做的那样,
从 ganache-cli
的命令行或从图形用户界面启动 ganache
区块链。
一旦 ganache
启动,我们就可以部署我们的METoken合约,看看是否一切都按预期工作:
$ truffle migrate --network ganache
Using network 'ganache'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0xb2e90a056dc6ad8e654683921fc613c796a03b89df6760ec1db1084ea4a084eb
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying METoken...
... 0xbe9290d59678b412e60ed6aefedb17364f4ad2977cfb2076b9b8ad415c5dc9f0
METoken: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在ganache
控制台上,我们应该看到我们的部署创建了4个新的交易:

Figure 2. METoken deployment on Ganache
# 使用Truffle控制台与METoken交互
我们可以使用Truffle控制台
与我们的ganache
区块链合同进行互动。这是一个交互式的JavaScript环境,可以访问Truffle环境,并
通过Web3访问区块链。在这种情况下,我们将Truffle控制台
连接到ganache
区块链:
$ truffle console --network ganache
truffle(ganache)>
2
truffle(ganache)>
提示符表明我们已连接到 ganache
区块链并准备输入我们的命令。Truffle控制台
支持所有的Truffle命令,
所以我们可以从控制台compile
和migrate
。我们已经运行过这些命令,所以让我们直接看看合同本身。METoken合约作为Truffle环
境内的JavaScript对象存在。在提示符下键入METoken
,它将转储整个合约定义:
truffle(ganache)> METoken
{ [Function: TruffleContract]
_static_methods:
[...]
currentProvider:
HttpProvider {
host: 'http://localhost:7545',
timeout: 0,
user: undefined,
password: undefined,
headers: undefined,
send: [Function],
sendAsync: [Function],
_alreadyWrapped: true },
network_id: '5777' }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
METoken
对象还公开几个属性,例如合同的地址(由migrate
命令部署):
[[METoken_address]]
truffle(ganache)> METoken.address
'0x345ca3e014aaf5dca488057592ee47305d9b3e10'
2
如果我们想要与已部署的合同进行交互,我们必须以JavaScript“promise”的形式使用异步调用。我们使用deployment
函数来获取合约
实例,然后调用totalSupply
函数:
truffle(ganache)> METoken.deployed().then(instance => instance.totalSupply())
BigNumber { s: 1, e: 9, c: [ 2100000000 ] }
2
接下来,让我们使用由ganache
创建的账户来检查我们的METoken余额并将一些METoken发送到另一个地址。首先,让我们获取帐户地址:
truffle(ganache)> let accounts
undefined
truffle(ganache)> web3.eth.getAccounts((err,res) => { accounts = res })
undefined
truffle(ganache)> accounts[0]
'0x627306090abab3a6e1400e9345bc60c78a8bef57'
2
3
4
5
6
7
accounts
列表现在包含由ganache
创建的所有帐户,而account[0]
是部署了该METoken合约的帐户。它应该有METoken的余额,因
为我们的METoken构造函数将全部Token提供给了创建它的地址。让我们检查:
truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(accounts[0]).then(console.log) })
undefined
BigNumber { s: 1, e: 9, c: [ 2100000000 ] }
2
3
最后,通过调用合约的 transfer
函数,让我们从account[0]
向 account[1]
转移1000.00 METoken:
truffle(ganache)> METoken.deployed().then(instance => { instance.transfer(accounts[1], 100000) })
undefined
truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(accounts[0]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 9, c: [ 2099900000 ] }
undefined
truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(accounts[1]).then(console.log) })
undefined
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }
2
3
4
5
6
7
8
9
10
11
TIP
METoken具有2位精度的小数,这意味着1个METoken在合同中是100个单位。当我们传输1000个METoken时,我们在传输函数中将该值指定 为100,000。
如你所见,在控制台中,account [0]
现在拥有20,999,000 MET,account [1]
拥有1000 MET。
如果切换到ganache
图形用户界面,你将看到名为transfer
函数的交易:

Figure 3. METoken transfer on Ganache
# 将ERC20Token发送到合同地址
到目前为止,我们已经设置了ERC20Token并从一个帐户转移到另一个帐户。我们用于这些示范的所有账户都是外部拥有账户(EOAs),这意 味着它们由私钥控制,而不是合同。如果我们将MET发送到合同地址会发生什么?让我们看看!
首先,我们将其他合约部署到我们的测试环境中。对于这个例子,我们将使用我们的第一个合同Faucet.sol
。我们将它添加到METoken项
目中,方法是将它复制到contracts
目录。我们的目录应该是这样的:
METoken/
├── contracts
│ ├── Faucet.sol
│ ├── METoken.sol
│ └── Migrations.sol
2
3
4
5
我们还会添加一个migration,从METoken
单独部署Faucet
:
var Faucet = artifacts.require("Faucet");
module.exports = function(deployer) {
// Deploy the Faucet contract as our only task
deployer.deploy(Faucet);
};
2
3
4
5
6
让我们从Truffle控制台编译和迁移合同:
$ truffle console --network ganache
truffle(ganache)> compile
Compiling ./contracts/Faucet.sol...
Writing artifacts to ./build/contracts
truffle(ganache)> migrate
Using network 'ganache'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x89f6a7bd2a596829c60a483ec99665c7af71e68c77a417fab503c394fcd7a0c9
Migrations: 0xa1ccce36fb823810e729dce293b75f40fb6ea9c9
Saving artifacts...
Running migration: 2_deploy_contracts.js
Replacing METoken...
... 0x28d0da26f48765f67e133e99dd275fac6a25fdfec6594060fd1a0e09a99b44ba
METoken: 0x7d6bf9d5914d37bcba9d46df7107e71c59f3791f
Saving artifacts...
Running migration: 3_deploy_faucet.js
Deploying Faucet...
... 0x6fbf283bcc97d7c52d92fd91f6ac02d565f5fded483a6a0f824f66edc6fa90c3
Faucet: 0xb18a42e9468f7f1342fa3c329ec339f254bc7524
Saving artifacts...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
现在让我们将一些MET发送到 Faucet
合约:
truffle(ganache)> METoken.deployed().then(instance => { instance.transfer(Faucet.address, 100000) })
truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(Faucet.address).then(console.log)})
truffle(ganache)> BigNumber { s: 1, e: 5, c: [ 100000 ] }
2
3
好的,我们已将1000 MET转移到 Faucet
合约。现在,我们如何从 Faucet
提款呢?
请记住,Faucet.sol
是一个非常简单的合同。它只有一个功能,withdraw
,这是提取_ether_。它没有提取MET或任何其他ERC20Token
的功能。如果我们使用withdraw
它将尝试发送ether,但由于Faucet还没有ether的余额,它将失败。
METoken
合约知道Faucet
有余额,但它可以转移该余额的唯一方法是它从合约地址收到transfer
调用。无论如何,我们需要让
Faucet
合约调用MET
中的transfer
函数。
如果你在思考下一步该做什么,不必了。这个问题没有解决办法。MET发送到Faucet
将永远卡住。只有Faucet
合约可以转让它,
Faucet
合约没有调用ERC20Token合约的transfer
函数的代码。
也许你预料到了这个问题。最有可能的是,你没有。实际上,数百名以太坊用户也无意将各种Token转让给没有任何ERC20能力的合同。 据估计,价值超过250万美元的Token被这样“卡住”,并且永远丢失。
ERC20Token的用户可能无意中在转移中丢失其Token的方式之一是当他们尝试转移到交易所或其他服务时。他们从交易所的网站上复制 以太坊地址,认为他们可以简单地向其发送Token。但是,许多交易所都公布实际上是合同的接收地址!这些合同具有许多不同的功能, 通常将发送给他们的所有资金清扫到“冷存储”或另一个集中的钱包。尽管有许多警告说“不要将Token发送到这个地址”,但许多Token 会以这种方式丢失。
# 演示 approve & transferFrom 流程
我们的Faucet
合同无法处理ERC20Token。使用transfer
函数发送Token给它,会导致这些Token的丢失。我们重写合同,并处理
ERC20Token。具体来说,我们将把它变成一个Faucet,将MET发给任何询问的人。
对于这个例子,我们制作了Truffle项目目录的副本,将其称为 METoken_METFaucet
,初始化Truffle,npm,安装OpenZeppelin
依赖项并复制METoken.sol
合同。有关详细说明,请参阅我们的第一个示例[发布我们自己的ERC20Token]。
现在,让我们创建一个新的Faucet合同,称之为METFaucet.sol
。它看起来像这样:
METFaucet.sol: a faucet for METoken
include::code/METoken_METFaucet/contracts/METFaucet.sol
// Version of Solidity compiler this program was written for
pragma solidity ^0.4.19;
import 'zeppelin-solidity/contracts/Token/ERC20/StandardToken.sol';
// A faucet for ERC20 Token MET
contract METFaucet {
StandardToken public METoken;
address public METOwner;
// METFaucet constructor, provide the address of METoken contract and
// the owner address we will be approved to transferFrom
function METFaucet(address _METoken, address _METOwner) public {
// Initialize the METoken from the address provided
METoken = StandardToken(_METoken);
METOwner = _METOwner;
}
function withdraw(uint withdraw_amount) public {
// Limit withdrawal amount to 10 MET
require(withdraw_amount <= 1000);
// Use the transferFrom function of METoken
METoken.transferFrom(METOwner, msg.sender, withdraw_amount);
}
// REJECT any incoming ether
function () public payable { revert(); }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
我们对基本的Faucet示例做了很多改动。由于METFaucet将使用METoken
中的transferFrom
函数,它将需要两个额外的变量。其中一个
将保存已部署的METoken
合约地址。另一个将持有MET所有者的地址,他们将提供Faucet提款的批准。METFaucet
将调用
METoken.transferFrom
并指示它将MET从所有者移至Faucet提取请求所来自的地址。
我们在这里声明这两个变量:
StandardToken public METoken;
address public METOwner;
2
由于我们的Faucet需要使用METoken
和METOwner
的正确地址进行初始化,因此我们需要声明一个自定义构造函数:
// METFaucet constructor, provide the address of METoken contract and
// the owner address we will be approved to transferFrom
function METFaucet(address _METoken, address _METOwner) public {
// Initialize the METoken from the address provided
METoken = StandardToken(_METoken);
METOwner = _METOwner;
}
2
3
4
5
6
7
8
下一个改变是withdraw
函数。METFaucet
使用METoken
中的transferFrom
函数,并要求METoken
将MET传输给Faucet的接收者,
而不是调用transfer
。
// Use the transferFrom function of METoken
METoken.transferFrom(METOwner, msg.sender, withdraw_amount);
2
最后,由于我们的Faucet不再发送ether,我们应该阻止任何人将ether送到METFaucet
,因为我们不希望它被卡住。我们更改fallback
函数以拒绝发进来的ether,使用revert
功能还原任何收款:
// REJECT any incoming ether
function () public payable { revert(); }
2
现在我们的METFaucet.sol
代码已准备就绪,我们需要修改migration脚本来部署它。这个migration脚本会有点复杂,因为METFaucet
依赖于METoken
的地址。我们将使用JavaScript promise按顺序部署这两个合约。创建2_deply_contracts.js
,如下所示:
var METoken = artifacts.require("METoken");
var METFaucet = artifacts.require("METFaucet");
var owner = web3.eth.accounts[0];
module.exports = function(deployer) {
// Deploy the METoken contract first
deployer.deploy(METoken, {from: owner}).then(function() {
// then deploy METFaucet and pass the address of METoken
// and the address of the owner of all the MET who will approve METFaucet
return deployer.deploy(METFaucet, METoken.address, owner);
});
}
2
3
4
5
6
7
8
9
10
11
12
13
现在,我们可以测试Truffle控制台中的所有内容。首先,我们使用migrate
来部署合同。当METoken
部署时,它会将所有MET分配给创
建它的帐户,web3.eth.accounts[0]
。然后,我们在METoken中调用approve
函数来批准METFaucet
代表web3.eth.accounts[0]
发送1000 MET。最后,为了测试我们的Faucet,我们从web3.eth.accounts[1]
调用METFaucet.withdraw
并尝试提取10个MET。以下
是控制台命令:
$ truffle console --network ganache
truffle(ganache)> migrate
Using network 'ganache'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x79352b43e18cc46b023a779e9a0d16b30f127bfa40266c02f9871d63c26542c7
Migrations: 0xaa588d3737b611bafd7bd713445b314bd453a5c8
Saving artifacts...
Running migration: 2_deploy_contracts.js
Replacing METoken...
... 0xc42a57f22cddf95f6f8c19d794c8af3b2491f568b38b96fef15b13b6e8bfff21
METoken: 0xf204a4ef082f5c04bb89f7d5e6568b796096735a
Replacing METFaucet...
... 0xd9615cae2fa4f1e8a377de87f86162832cf4d31098779e6e00df1ae7f1b7f864
METFaucet: 0x75c35c980c0d37ef46df04d31a140b65503c0eed
Saving artifacts...
truffle(ganache)> METoken.deployed().then(instance => { instance.approve(METFaucet.address, 100000) })
truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(web3.eth.accounts[1]).then(console.log) })
truffle(ganache)> BigNumber { s: 1, e: 0, c: [ 0 ] }
truffle(ganache)> METFaucet.deployed().then(instance => { instance.withdraw(1000, {from:web3.eth.accounts[1]}) } )
truffle(ganache)> METoken.deployed().then(instance => { instance.balanceOf(web3.eth.accounts[1]).then(console.log) })
truffle(ganache)> BigNumber { s: 1, e: 3, c: [ 1000 ] }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
从结果中可以看出,我们可以使用 approve
and transferFrom
工作流来授权一个合约转移另一个Token中定义的Token。如果使用
得当,ERC20Token可以由EOA和其他合同使用。
但是,正确管理ERC20Token的负担会推送到用户界面。如果用户错误地尝试将ERC20Token转移到合同地址,并且该合同没有配备接收 ERC20Token的功能,则Token将丢失。
# ERC20Token的问题
ERC20Token标准的采用确实是爆炸性的。成千上万的Token已经启动,既可以尝试新的功能,也可以通过各种“众筹”拍卖和初始投币产品 (ICO)筹集资金。然而,正如我们在将Token转移到合同地址的问题所看到的那样,存在一些潜在的陷阱。
ERC20Token不太明显的问题之一是它们暴露了Token和ether本身之间的细微差别。如果ether通过以接收者地址为目的地的交易转移, 则Token转移发生在 specific Token contract state 中,并且将Token合同作为其目的地,而不是接收者的地址。Token合同 跟踪余额并发布事件。在Token传输中,实际上没有交易发送给Token的接收者。相反,接收者的地址将被添加到Token合约本身的映 射中。将ether发送到地址的交易会改变地址的状态。将Token转移到地址的交易只会改变Token合约的状态,而不会改变接收者地址 的状态。即使是支持ERC20Token的钱包,也不会意识到Token的余额,除非用户明确将特定Token合约添加到“监视”中。一些钱包观 察最受欢迎的Token合约,以检测由他们控制的地址持有的余额,但这仅限于ERC20合同的一小部分。
事实上,用户不太可能会追踪所有可能的ERC20Token合约中的所有余额。许多ERC20Token更像是垃圾邮件,而不是可用的Token。 他们自动为拥有ether活动的帐户创建余额,以吸引用户。如果你有一个活动历史悠久的以太坊地址,特别是如果它是在预售中创 建的,你会发现它充满了凭空出现的“垃圾”Tokens。当然,这个地址并不是真的充满了Token,而是那些Token合约有你的地址。 如果你用于查看地址的资源管理器或钱包正在监视这些Token合约,才能看到这些余额。
Token不像ether。Ether通过send
功能发送,并由合同中的任何payable函数或任何EOA接受。Token仅使用在ERC20合同中
存在的transfer
或approve&transferFrom
函数发送,并且不会(至少在ERC20中)触发收款合同中的任何payable函
数。Token的功能就像ether这样的加密货币,但它们带有一些细微的区别,可以打破这种错觉。
考虑另一个问题。要发送ether,或使用任何以太坊合同,你需要ether来支付gas。发送Token,你_也需要ether_。你无法 用Token支付交易的gas,而Token合同也无法为你支付gas费用。这可能会导致一些相当奇怪的用户体验。例如,假设你使用 交易所或Shapeshift将某些比特币转换为Token。你在钱包中“收到”该Token,该钱包会跟踪该Token的合同并显示你的余额。 它看起来与你钱包中的任何其他加密货币相同。现在尝试发送Token,你的钱包会通知你,你需要ether才能这样做。你可能会 感到困惑 - 毕竟你不需要ether接收Token。也许你没有ether。也许你甚至不知道该Token是以太坊上的ERC20Token,也许 你认为这是一个拥有自己的区块链的加密货币。错觉就这样被打破了。
其中一些问题是ERC20Token特有的。其他更一般的问题涉及到以太坊内的抽象和界面边界。有些可以通过更改Token接口来解决, 其他可能需要更改以太坊内的基础结构(例如EOAs和合同之间以及交易和消息之间的区别)。有些可能不完全“可解决”,并且可 能需要用户界面设计来隐藏细微差别并使用户体验一致,而不管其底层区别如何。
在接下来的部分中,我们将看看试图解决其中一些问题的各种提案。
# ERC223 - 一种建议的Token合同接口标准
ERC223提案试图通过检测目的地地址是否是合同来解决无意中将Token转移到合同(可能支持或不支持Token)的问题。ERC223要
求用于接受Token的契约实现名为TokenFallback
的函数。如果传输的目的地是合同并且合同不支持Token(即不实现TokenFallback
)
,则传输失败。
为了检测目标地址是否为契约,ERC223参考实现使用了一小段内联字节码,并采用了一种颇具创造性的方式:
function isContract(address _addr) private view returns (bool is_contract) {
uint length;
assembly {
//retrieve the size of the code on target address, this needs assembly
length := extcodesize(_addr)
}
return (length>0);
}
2
3
4
5
6
7
8
你可以在这里看到有关ERC223提案的讨论:
https://github.com/ethereum/EIPs/issues/223 (opens new window)
ERC223合同接口规范是:
interface ERC223Token {
uint public totalSupply;
function balanceOf(address who) public view returns (uint);
function name() public view returns (string _name);
function symbol() public view returns (string _symbol);
function decimals() public view returns (uint8 _decimals);
function totalSupply() public view returns (uint256 _supply);
function transfer(address to, uint value) public returns (bool ok);
function transfer(address to, uint value, bytes data) public returns (bool ok);
function transfer(address to, uint value, bytes data, string custom_fallback) public returns (bool ok);
event Transfer(address indexed from, address indexed to, uint value, bytes indexed data);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ERC223没有得到广泛的实施,ERC讨论中有一些关于向前兼容性和在合同接口级别或用户界面上实现更改之间的折衷的争论。争论仍在继续。
# ERC777 - 一种建议的Token合同接口标准
另一项改进Token合同标准的提案是ERC777。该提案有几个目标,包括:
- 提供ERC20兼容性界面
- 使用
send
功能传输Token,类似于ether传输 - 与ERC820兼容Token合同注册
- 合同和地址可以通过在发送之前调用
TokensToSend
函数来控制它们发送的Token - 通过在接收者中调用
TokensReceived
函数来通知合同和地址 - Token传输交易包含
userData
和operatorData
字段中的元数据 - 无论是发送到合同还是EOA,都以相同的方式运作
有关ERC777的详细信息和正在进行的讨论可以在这里找到:
https://github.com/ethereum/EIPs/issues/777 (opens new window)
ERC777合同接口规范是:
interface ERC777Token {
function name() public constant returns (string);
function symbol() public constant returns (string);
function totalSupply() public constant returns (uint256);
function granularity() public constant returns (uint256);
function balanceOf(address owner) public constant returns (uint256);
function send(address to, uint256 amount) public;
function send(address to, uint256 amount, bytes userData) public;
function authorizeOperator(address operator) public;
function revokeOperator(address operator) public;
function isOperatorFor(address operator, address TokenHolder) public constant returns (bool);
function operatorSend(address from, address to, uint256 amount, bytes userData, bytes operatorData) public;
event Sent(address indexed operator, address indexed from, address indexed to, uint256 amount, bytes userData, bytes operatorData);
event Minted(address indexed operator, address indexed to, uint256 amount, bytes operatorData);
event Burned(address indexed operator, address indexed from, uint256 amount, bytes userData, bytes operatorData);
event AuthorizedOperator(address indexed operator, address indexed TokenHolder);
event RevokedOperator(address indexed operator, address indexed TokenHolder);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ERC777的参考实现与提案相关联。ERC777依赖于ERC820中关于注册合同的并行提案。关于ERC777的一些争论是关于同时采用两个大变化 的复杂性:一个新的Token标准和一个注册标准。讨论仍在继续。
# ERC721 - 不可替代的Token(契据)标准
我们目前看到的所有Token标准都是_可互换_Token,这意味着Token的每个单元都是完全可以互换的。ERC20Token标准仅跟踪每个帐户 的最终余额,并且(明确地)跟踪任何Token的出处。
ERC721提案是_不可互换的_ Tokens标准,也称为 契据 deeds。
牛津词典:
契约:签署和交付的法律文件,尤其是关于财产或合法权利所有权的法律文件。
契约一词的使用旨在反映“财产所有权”部分,即使这些部分在任何司法管辖区都不被承认为“法律文件”,至少目前不是。
不可互换的Token追踪独特事物的所有权。拥有的东西可以是数字项目,例如游戏物品或数字收藏品。或者,这种东西可以是物理事物, 其物主通过Token进行跟踪,例如房屋,汽车,艺术品等。契约也可以代表负值的东西,例如贷款(债务),留置权,地役权等。 ERC721标准对所有权由契约追踪的事物的性质没有限制或期望,只是它可以是唯一标识,在这个标准的情况下是由256位标识符实现的。
标准和讨论的细节在两个不同的GitHub位置进行跟踪:
初步建议: https://github.com/ethereum/EIPs/issues/721 (opens new window)
继续讨论: https://github.com/ethereum/EIPs/pull/841 (opens new window)
要掌握ERC20和ERC721之间的基本差异,只需查看ERC721中使用的内部数据结构即可:
// Mapping from deed ID to owner
mapping (uint256 => address) private deedOwner;
2
ERC20跟踪属于每个所有者的余额,所有者是映射的主键,ERC721跟踪每个契约ID以及谁拥有它,契约ID是映射的主键。从这个基本 差异衍生出不可替代的Token的所有属性。
ERC721 合同接口规范是:
interface ERC721 /* is ERC165 */ {
event Transfer(address indexed _from, address indexed _to, uint256 _deedId);
event Approval(address indexed _owner, address indexed _approved, uint256 _deedId);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
function balanceOf(address _owner) external view returns (uint256 _balance);
function ownerOf(uint256 _deedId) external view returns (address _owner);
function transfer(address _to, uint256 _deedId) external payable;
function transferFrom(address _from, address _to, uint256 _deedId) external payable;
function approve(address _approved, uint256 _deedId) external payable;
function setApprovalForAll(address _operateor, boolean _approved) payable;
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
2
3
4
5
6
7
8
9
10
11
12
13
ERC721还支持两个 可选 接口,一个用于元数据,一个用于枚举契约和所有者。
用于元数据的ERC721可选接口是:
interface ERC721Metadata /* is ERC721 */ {
function name() external pure returns (string _name);
function symbol() external pure returns (string _symbol);
function deedUri(uint256 _deedId) external view returns (string _deedUri);
}
2
3
4
5
用于枚举的ERC721可选接口是:
interface ERC721Enumerable /* is ERC721 */ {
function totalSupply() external view returns (uint256 _count);
function deedByIndex(uint256 _index) external view returns (uint256 _deedId);
function countOfOwners() external view returns (uint256 _count);
function ownerByIndex(uint256 _index) external view returns (address _owner);
function deedOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256 _deedId);
}
2
3
4
5
6
7
# Token标准
在本节中,我们回顾几个建议的标准和几个广泛部署的Token合约标准。这些标准究竟做了什么?你应该使用这些标准吗?你应该如何 使用它们?你应该添加超出这些标准的功能吗?你应该使用哪些标准?接下来我们将研究所有这些问题。
# 什么是Token标准?他们的目的是什么?
Token标准是实现的_最小_规范。这意味着为了符合ERC20的要求,你至少需要实施ERC20规定的功能和行为。你还可以通过实现不属于 标准的功能来自由添加功能。
这些标准的主要目的是鼓励合同之间的互用性。因此,所有钱包,交易所,用户界面和其他基础设施组件都可以以可预见的方式与任何遵 循规范的合同进行交流。
标准的目的是 描述性的 descriptive,而不是 规定性的 prescriptive 。你如何选择实现这些功能取决于你 - 合同的内部 功能与标准无关。它们有一些功能要求,它们管理特定情况下的行为,但它们没有规定实现。例如,如果值设置为零,则传递函数的行为。
# 你应该使用这些标准吗?
鉴于所有这些标准,每个开发人员都面临两难选择:使用现有标准还是创新超出他们施加的限制?
这种困境并不容易解决。标准必然会限制你的创新能力,创造一个你必须遵循的狭窄“车辙”。另一方面,基本标准来自数百个应用程序的 经验,并且通常与99%的用例非常吻合。
作为这一考虑的一部分,还有一个更大的问题:互操作性和广泛采用的价值。如果你选择使用现有标准,你将获得设计用于该标准的所有 系统的价值。如果你选择偏离标准,则必须考虑自己构建所有基础架构的成本,或者说服其他人支持你新标准的实施。建立自己的道路并 忽视现有标准的倾向被称为“Not Invented Here”,并且与开源文化相对立。另一方面,进步和创新有时依靠背离传统。这是一个棘手 的选择,所以仔细考虑吧!
维基百科“Not Invented Here”(https://en.wikipedia.org/wiki/Not_invented_here (opens new window))
Not Invented Here是由社会,企业或机构文化采取的立场,由于其外部起源和成本(如版税),避免使用或购买已有产品,研究,标准 或知识。
# 安全成熟度
除了标准的选择之外,还有_implementation_的并行选择。当你决定使用标准(如ERC20)时,你必须决定如何实施兼容Token。以太坊 生态系统中广泛使用了一些现有的“参考”实现。或者你可以从头开始写你自己的。再次,这个选择代表了一个可能产生严重安全隐患的困境。
现有的实施是“战斗测试”。虽然不可能证明它们是安全的,但其中许多都支持数百万美元的Token。他们一再受到攻击,并且受到了强烈的 攻击。到目前为止,没有发现重大的漏洞。写你自己的并不容易 - 有许多微妙的方式可以让合约受到损害。使用经过充分测试的广泛使用 的实现更加安全。在我们上面的例子中,我们使用了ERC20标准的OpenZeppelin实现,因为这个实现从头到尾都是安全的。
如果你使用现有的实现,你也可以扩展它。再次小心这种冲动。复杂性是安全的敌人。你添加的每一行代码都会扩展合约的 受攻击面 , 并可能代表处于等待状态的漏洞。你可能没有注意到一个问题,直到你在合同上投入了很多价值并且有人打破了它。
# Token接口标准的扩展
本节中讨论的Token标准以非常小的接口开始,功能有限。许多项目已经创建了扩展实现,以支持他们的应用程序所需的功能。其中一些包 括:
所有者控制
特定地址或一组地址(多重签名)具有特殊功能,例如黑名单,白名单,铸造,恢复等。
燃烧
Token燃烧是指Token被转移到不可靠的地址或删除余额并减少供应时故意销毁。
Minting
以可预测的速率添加Token的总供应量的能力,或通过Token创建者的“命令”添加的能力。
Crowdfunding
提供Token销售的能力,例如通过拍卖,市场销售,反向拍卖等。
上限
总供给的预定义和不可改变的限制,与“minting”功能相反。
恢复“后门”
恢复资金,反向传输或拆除由指定地址或一组地址激活的Token(多重签名)的功能。
白名单
限制Token传输仅限于列出的地址的功能。在经过不同司法管辖区的规则审核后,最常用于向“经认可的投资者”提供Token。
通常有一种更新白名单的机制。
黑名单
通过禁止特定地址来限制Token传输的能力。通常有更新黑名单的功能。
在OpenZeppelin库中有许多这些功能的参考实现。其中一些是面向特定用例的,仅在少数Token中实现。到目前为止,这些功能的 接口还没有被广泛接受的标准。
如前所述,扩展具有附加功能的Token标准的决定代表了创新/风险与互操作性/安全性之间的权衡。
# Tokens 和 ICOs
Token已经成为以太坊生态系统中的爆炸性发展。它们可能是所有智能合约平台(如以太坊)中非常重要的基础组件。
尽管如此,这些标准的重要性和未来影响不应该与当前Token产品的认可相混淆。就像在任何早期阶段的技术一样,第一波产品和公司几乎 都会失败。今天在Ethereum上提供的许多Token几乎都是伪装的骗局,传销和金钱争夺。
诀窍是将这种技术的长期愿景和影响与可能非常大的Token ICO的短期泡沫区分开来,这种泡沫充斥着欺诈行为。两者在同一时间都可以是 真实的。Token标准和平台将在目前的Token狂热中幸存下来,然后他们可能会改变世界。
# 第十一章 去中心化应用(DApps)
点对点(P2P,peer-to-peer)运动使数百万互联网用户能够连接在一起。USENET是被称为第一个点对点架构的一种分布式消息传递系统, 它于1979年成立,是第一个“互联网”ARPANET的继承者。ARPANET是一个客户端 - 服务器网络,参与者运行节点请求和提供内容,但由于 除了简单的基于地址的路由,缺乏提供任何上下文的能力,USENET很有希望实施一个分散的控制模型,即客户端 - 服务器模型,分别从用 户或客户角度为新闻组服务器提供自组织方法。
1999年,著名的音乐和文件共享应用程序Napster出现了。Napster是点对点网络运动演变为BitTorrent的开始,参与用户建立了一个虚拟 网络,完全独立于物理网络,无需遵守任何管理机构或限制。
由于点对点机制可用于访问任何类型的分布式资源,因此它们在去中心化应用程序中起着核心作用。
# 什么是DApp?
与传统应用程序不同,去中心化应用(DApp)不仅属于单个提供者或服务器,而是整个栈将在P2P网络上以分布式方式部署和操作。
典型的DApp栈包括前端,后端和数据存储。创建DApp有许多优点,典型集中式架构无法提供:
1)弹性:在智能合约上编写业务逻辑意味着DApp后端将在区块链上完全分发和管理。与在中央服务器上部署应用程序不同,DApp不会有 停机时间,只要区块链仍在运行,它就会继续存在。
2)透明性:DApp的开源特性允许任何人分叉代码并在区块链上运行相同的应用程序。同样,任何与区块链的互动都将永久存储,任何拥 有区块链副本的人都可以获得对它的访问权限。值得注意的是,可能无法将字节码反编译为源码并完全理解合约的代码。寻求提供合约行 为完全透明的开发人员必须发布供用户阅读,编译和验证的源代码。
3)抗审查:只要用户可以访问以太坊节点,用户将始终能够与DApp交互而不受集中机构控制的干扰。一旦在网络上部署代码,任何服务 提供商,甚至智能合约的所有者都不能更改代码。
# DApp的组件
# 区块链(智能合约)
智能合约用于存储去中心化应用程序的业务逻辑,状态和计算; 将智能合约视为常规应用程序中的服务器端组件。
在以太坊智能合约上部署服务器端逻辑的一个优点是,你可以构建一个更复杂的架构,智能合约可以在其中相互读取和写入数据。部署智能 合约后,未来许多其他开发人员都可以使用你的业务逻辑,而无需你管理和维护代码。
将智能合约作为核心业务逻辑功能运行的一个主要问题是在部署代码后无法更改代码。此外,一个非常庞大的智能合约可能需要耗费大量 gas来部署和运行。因此,某些应用程序可能会选择离线计算和外部数据源。请记住,DApp的核心业务逻辑依赖于外部数据或服务器意味 着你的用户必须信任这些外部事物。
# 前端(Web用户界面(UI))
与DApp的业务逻辑需要开发人员了解EVM和新语言(如Solidity)不同,DApp的客户端界面使用基本的Web前端技术(HTML,CSS, JavaScript)。这允许传统的Web开发人员使用他们熟悉的工具,库和框架。与DApp的交互(例如签名消息,发送交易和密钥管理)通常 通过浏览器本身使用Mist浏览器或Metamask浏览器扩展等工具进行。
虽然也可以创建移动DApp,但由于缺少可用作具有密钥管理功能的轻客户端的移动客户端,目前没有创建移动DApp前端的最佳实践。
# 数据存储
由于gas成本高,智能合约目前不适合存储大量数据。因此,大多数DApps将利用去中心化存储(如IPFS或Swarm)来存储和分发大型静态 资产,如图像,视频和客户端应用程序(HTML,CSS,JavaScript)。
内容的哈希值通常使用键值映射存储为智能合约中的字节。然后,通过你的前端应用程序调用智能合约检索资产,以获取每个资产的URL。
# 上链 vs. 脱链
# IPFS
# Swarm
Swarm主页: http://swarm-gateways.net/bzz:/theswarm.eth/ (opens new window)
阅读文档: https://swarm-guide.readthedocs.io/en/latest/index.html (opens new window)
Swarm开发人员的入门指南: https://github.com/ethersphere/swarm/wiki/swarm (opens new window)
Swarm讨论组: https://gitter.im/ethersphere/orange-lounge (opens new window)
Ethereum's Swarm 和 IPFS 的相似之处与不同之处; https://github.com/ethersphere/go-ethereum/wiki/IPFS-&-SWARM (opens new window)
# 中心化数据
集中式数据库是服务器上的数据存储,其特征与描述相关联。它们使用客户端 - 服务器网络架构,这允许客户端或用户修改存储在集 中式服务器上的数据。数据库的控制权仍由指定的管理员进行,管理员在提供对数据库的访问之前对客户端的凭据进行身份验证。如果 管理员的安全性受到损害,则可以更改甚至删除数据,因为管理员是唯一负责管理数据库的人。
# DApp框架
有许多不同的开发框架和库,以多种语言编写,使得开发人员可以在创建和部署DApp时获得更好的体验。
# Truffle
Truffle是一种流行的选择,为以太坊提供可管理的开发环境,测试框架和资产管道。
有了Truffle,你会得到:
- 内置智能合约编译,链接,部署和二进制管理。
- 与Mocha和Chai进行自动合约测试。
- 可配置的构建管道,支持自定义构建过程。
- 可编写脚本的部署和迁移框架。
- 用于部署到许多公共和专用网络的网络管理。
- 直接与合约沟通的交互式控制台。
- 在开发过程中即时重建资产。
- 在Truffle环境中执行脚本的外部脚本运行器。
入门和文档:http://truffleframework.com/docs (opens new window)
Github:(https://github.com/trufflesuite/truffle)(https://github.com/trufflesuite/truffle)
Website:https://truffleframework.com (opens new window)
# Embark Embark框架专注于使用以太坊,IPFS和其他平台的无服务器去中心化应用。Embark目前与EVM区块链(Ethereum),去中心化存储(IPFS)和去中心化通信平台(Whisper和Orbit)集成。
区块链(以太坊)
- 自动部署合约并使其在JS代码中可用。启动监视更改,如果你更新合约,Embark将自动重新部署合约(如果需要)和DApp。
- JS通过Promises使用合约。
- 使用Javascript与合约进行测试驱动开发。
- 跟踪已部署的合约; 只在真正需要时部署。
- 管理不同的链(例如,测试网,私人网,livenet)
- 轻松管理相互依赖合约的复杂系统。
去中心化存储(IPFS)
- 通过EmbarkJS轻松存储和检索DApp上的数据,包括上传和检索文件。
- 将完整的应用程序部署到IPFS或Swarm。
去中心化通信 (Whisper, Orbit)
通过Whisper或Orbit轻松通过P2P渠道发送/接收消息。
网络技术
与任何网络技术集成,包括React,Foundation等。
使用你想要的任何构建管道或工具,包括grunt,gulp和webpack。
入门和文档:https://embark.readthedocs.io (opens new window)
Github:https://github.com/embark-framework/embark (opens new window)
Website:https://github.com/embark-framework/embark (opens new window)
# Emerald
Emerald Platform 是一个框架和工具集,用于简化Dapps的开发以及现有服务与基于以太坊的区块链的集成。
Emerald提供:
- Javascript库和React组件构建Dapp
- 区块链项目常见的SVG图标
- 用于管理私钥的Rust库,包括硬件钱包和签名交易
- 可以集成到现有app命令行或JSON RPC API中的现成的组件和服务
- SputnikVM,一个独立的EVM实现,可用于开发和测试
它与平台无关,为各种目标提供工具:
- 与Electron捆绑的桌面应用程序
- 移动应用程序
- 网络应用程序
- 命令行应用程序和脚本工具
入门和文档:https://docs.etcdevteam.com (opens new window)
Github:https://github.com/etcdevteam/emerald-platform (opens new window)
Website:https://emeraldplatform.io (opens new window)
# DApp(开发工具)
DApp是一个用于智能合约开发的简单命令行工具。它支持以下常见用例:
- 包管理
- 源代码构建
- 单元测试
- 简单的合约部署
入门和文档:[https://dapp.readthedocs.io/en/latest/]
# 活跃的DApps
以下列出了以太坊网络上的活跃DApp:
# EthPM
一个旨在将包管理带入以太坊生态系统的项目。
Website:https://www.ethpm.com/
# Radar Relay
DEX(去中心化交易所)专注于直接从钱包到钱包交易基于以太坊的tokens。
Website:https://radarrelay.com/
# CryptoKitties
在以太坊上部署的游戏,允许玩家购买,收集,繁殖和销售各种类型的虚拟猫 它代表了为休闲和悠闲目的部署区块链技术的最早尝试之一。
Website:https://www.cryptokitties.co
# Ethlance
Ethlance是一个连接自由职业者和开发者的平台,用ether支付和收款。
# Decentraland
Decentraland是以太坊区块链支持的虚拟现实平台。用户可以创建,体验内容和应用程序并从中获利。
Website:https://decentraland.org/
# 第十二章 预言机(Oracles)
# Oracles
以太坊虚拟机的一个关键属性是它能够以完全确定的方式执行智能合约字节码。EVM保证相同的操作将返回相同的输出,而不管它们实际 运行的计算机。这一特性虽然是以太坊安全保证的关键,但它通过阻止智能合约检索和处理脱链数据来限制智能合约的功能。
但是,许多区块链应用程序需要访问外部信息。这就是 oracles 发挥作用的地方。可以将Oracles定义为离线数据的权威来源,允许智能 合约使用外部信息接收和条件执行 - 它们可以被视为弥合链上链下之间鸿沟的机制。允许智能合约根据实际事件和数据强制执行合约关系, 大大扩展了其范围。可能由oracles提供的数据示例包括:
- 来自物理来源的随机数/熵(例如量子/热现象):公平地选择彩票智能合约中的赢家
- 与自然灾害相关的参数触发器:触发巨灾债券智能合约(对于飓风债券来说的风速)
- 汇率数据:将稳定币与法定货币准确挂钩
- 资本市场数据:代币化资产/证券的定价篮子
- 基准参考数据:将利率纳入智能金融衍生品
- 静态/伪静态数据:安全标识符,国家/地区代码,货币代码
- 时间和间隔数据:事件触发器以精确的SI时间测量为基础
- 天气数据:基于天气预报的保险费计算
- 政治事件:预测市场决议
- 体育赛事:预测市场决议和幻想体育合约
- 地理位置数据:供应链跟踪
- 损害赔偿:保险合约
- 其他区块链上发生的事件:互操作函数
- 交易天然气价格:gas价格oracles
- 航班延误:保险合约
在本节中,我们将在Solidity中检查oracles的主要功能,oracle订阅模式,计算oracles,去中心化的oracles和oracle客户端实现。
# 主要功能
实际上,oracle可以实现为链上智能合约系统和用于监控请求,检索和返回数据的离线基础设施。来自去中心化应用的数据请求通常是涉及 许多步骤的异步过程。
首先,外部所有帐户将与去中心化应用进行交易,从而与oracle智能合约中定义的函数进行交互。此函数初始化对oracle的请求,除了可能 包含回调函数和调度参数的补充信息之外,还使用相关参数详细说明所请求的数据。一旦验证了此事务,就可以将oracle请求视为oracle 合约发出的EVM事件,或者状态更改; 参数可以被取出并用于从脱链数据源执行实际查询。oracle可能需要处理请求的费用,回调的gas费用 ,以及访问所请求数据的权限/权限。最后,结果数据由oracle所有者签署,基本上证明在给定时间数据的价值,并在交易中交付给作出请求 的去中心化应用 - 直接或通过oracle合约。根据调度参数,oracle可以定期广播进一步更新数据的事务,例如日终定价信息。
可能有一系列替代方案。可以从外部所有帐户请求数据并直接返回数据,从而无需使用oracle智能合约。类似地,可以向物联网启用的硬件 传感器发出请求和响应。因此,oracles可以是人类,软件或基于硬件的。
oracle的主要功能可概括如下:
- 回应去中心化应用的查询
- 解析查询
- 检查是否符合付款和数据权限/权利义务
- 从脱链源检索数据
- 在交易中签署数据
- 向网络广播交易
- 进一步安排交易
# 订阅模式
上述订阅模式是典型的请求——响应模式,常见于客户端——服务器体系结构中。虽然这是一种有用的消息传递模式,允许应用程序进行双向 对话,但它是一种相对简单的模式,在某些条件下可能不合适。例如,需要oracle提供利率的智能债券可能需要在请求-响应模式下每天 请求数据,以确保利率始终是正确的。鉴于利率不经常变化,发布——订阅模式在这里可能更合适,尤其是考虑到以太坊的有限带宽。
发布——订阅是一种模式,其中发布者,这里是oracles,不直接向接收者发送消息,而是将发布的消息分到不同的类中。订阅者能够表达 对一个或多个类的兴趣并仅检索那些感兴趣的消息。在这种模式下,oracle可以将利率写入其自己的内部存储,当且仅当它发生变化时。 多个订阅的去中心化应用可以简单地从oracle合约中读取它,从而减少对网络带宽的影响,同时最大限度地降低存储成本。
在广播或多播模式中,oracle会将所有消息发布到一个频道,订阅合约将在各种订阅模式下收听该频道。例如,oracle可能会将消息发布 到加密货币汇率通道。订阅智能合约如果需要时间序列,例如移动平均计算,则可以请求信道的全部内容; 另一个可能只需要最后一个价格 来计算现货价格。在oracle不需要知道订阅合约的身份的情况下,广播模式是合适的。
# 数据认证
如果我们假设去中心化应用查询的数据源既具有权威性又值得信赖,那么一个悬而未决的问题仍然存在:假设oracle和查询/响应机制可能 由不同的实体操作,我们如何才能信任这种机制?数据在传输过程中可能会被篡改,因此脱链方法能够证明返回数据的完整性至关重要。 数据认证的两种常用方法是真实性证明和可信执行环境(TEE)。
真实性证明是加密保证,证明数据未被篡改。基于各种证明技术(例如,数字签名证明),它们有效地将信任从数据载体转移到证明者, 即证明方法的提供者。通过在链上验证真实性,智能合约能够在对其进行操作之前验证数据的完整性。Oraclize[1]是利用各种真实性 证明的oracle服务的一个例子。目前可用于以太坊主网络的数据查询是TLSNotary Proof [2]。TLSNotary Proofs允许客户端向第 三方提供客户端和服务器之间发生HTTPS Web流量的证据。虽然HTTPS本身是安全的,但它不支持数据签名。结果是,TLSNotary证明 依赖于TLSNotary(通过PageSigner [3])签名。TLSNotary Proofs利用传输层安全性(TLS)协议,使得在访问数据后对数据进 行签名的TLS主密钥在三方之间分配:服务器(oracle),受审核方(Oraclize)和核数师。Oraclize使用Amazon Web Services (AWS)虚拟机实例作为审核员,可以证明自它实例化以来未经修改[4]。此AWS实例存储TLSNotary机密,允许其提供诚实证明。虽然 它提供了比纯信任查询/响应机制更高的数据篡改保证,但这种方法确实需要假设亚马逊本身不会篡改VM实例。
TownCrier [5,6]是基于可信执行环境的经过身份验证的数据馈送oracle系统; 这些方法采用不同的机制,利用基于硬件的安全区域来 验证数据的完整性。TownCrier使用英特尔的SGX(Software Guard eXtensions)来确保HTTPS查询的响应可以被验证为可靠。SGX提 供完整性保证,确保在安全区内运行的应用程序受到CPU的保护,防止任何其他进程被篡改。它还提供机密性,确保在安全区内运行时应 用程序的状态对其他进程不透明。最后,SGX允许证明,通过生成数字签名的证据,证明应用程序 - 通过其构建的哈希安全地识别 - 实 际上是在安全区内运行。通过验证此数字签名,去中心化式应用程序可以证明TownCrier实例在SGX安全区内安全运行。反过来,这证明 实例没有被篡改,因此TownCrier发出的数据是真实的。机密性属性还允许TownCrier通过允许使用TownCrier实例的公钥加密数据查询 来处理私有数据。通过在诸如SGX的安全区内运行oracle的查询/响应机制,可以有效地将其视为在受信任的第三方硬件上安全运行,确 保所请求的数据被返回到未被禁用的状态(假设我们信任Intel/SGX)。
# 计算 oracles
到目前为止,我们只是在请求和提供数据的背景下讨论了oracles。然而,oracles也可用于执行任意计算,这一功能在以太坊固有的区块 gas限制和相对昂贵的计算成本的情况下特别有用; Vitalik本人指出,与现有的集中服务相比,以太坊的计算成本效率低了一百万倍[7]。 计算oracles可以用于对一组输入执行相关计算,而不是仅仅中继查询结果,并返回计算结果,这可能是在链上计算不可行的。例如,可以 使用计算oracle执行计算密集型回归计算,以估计债券合约的收益率。
Oraclize提供的服务允许去中心化应用请求输出在沙盒AWS虚拟机中执行的计算。AWS实例从包含在上传到IPFS的存档中的用户配置的 Dockerfile创建可执行容器。根据请求,Oraclize使用其哈希检索此存档,然后在AWS上初始化并执行Docker容器,将作为环境变量提供 给应用程序的任何参数传递。容器化应用程序根据时间限制执行计算,并且必须将结果写入标准输出,Oraclize可以将其返回到去中心化 应用。Oraclize目前在可审核的t2.micro AWS实例上提供此服务。
作为可验证的oracle真理的标准,“cryptlet”的概念已被正式化为Microsoft更广泛的ESC框架[8]的一部分。Cryptlet在加密的封装内 执行,该封装抽象出基础设施,例如I/O,并附加了CryptoDelegate,以便自动对传入和传出的消息进行签名,验证和验证。Cryptlet 支持分布式事务,因此合约逻辑可以以ACID方式处理复杂的多步骤,多区块链和外部系统事务。这允许开发人员创建便携,隔离和私有的 真相解决方案,以便在智能合约中使用。Cryptlet遵循以下格式:
public class SampleContractCryptlet : Cryptlet
{
public SampleContractCryptlet(Guid id, Guid bindingId, string name, string address, IContainerServices hostContainer, bool contract)
: base(id, bindingId, name, address, hostContainer, contract)
{
MessageApi =
new CryptletMessageApi(GetType().FullName, new SampleContractConstructor())
2
3
4
5
6
7
TrueBit [9]是可扩展和可验证的离线计算的解决方案。它引入了一个求解器和验证器系统,分别执行计算和验证。如果解决方案受到挑战, 则在链上执行对计算子集的迭代验证过程 - 一种“验证游戏”。游戏通过一系列循环进行,每个循环递归地检查计算的越来越小的子集。游戏 最终进入最后一轮,挑战是微不足道的,以至于评委 - 以太坊矿工 - 可以对挑战是否合理,在链上进行最终裁决。实际上,TrueBit是一个 计算市场的实现,允许去中心化应用支付可在网络外执行的可验证计算,但依靠以太坊来强制执行验证游戏的规则。理论上,这使无信任的 智能合约能够安全地执行任何计算任务。
TrueBit等系统有广泛的应用,从机器学习到任何工作量证明的验证。后者的一个例子是Doge-Ethereum桥,它利用TrueBit来验证 Dogecoin的工作量证明,Scrypt,一种难以在以太坊块gas限制内计算的内存密集和计算密集型函数。通过在TrueBit上执行此验证, 可以在以太坊的Rinkeby测试网络上的智能合约中安全地验证Dogecoin交易。
# 去中心化的 oracles
上面概况的机制都描述了依赖于可信任权威的集中式oracle系统。虽然它们应该足以满足许多应用,但它们确实代表了以太坊网络中的 中心故障点。已经提出了许多围绕去中心化oracle作为确保数据可用性手段的方案,以及利用链上数据聚合系统创建独立数据提供者网络。
ChainLink [10]提出了一个去中心化oracle网络,包括三个关键的智能合约:信誉合约,订单匹配合约,汇总合约和数据提供商的脱链 注册。信誉合约用于跟踪数据提供商的绩效。声誉合约中的分数用于填充离线注册表。订单匹配合约使用信誉合约从oracles中选择出价。 然后,它最终确定服务级别协议(SLA),其中包括查询参数和所需的oracles数量。这意味着购买者无需直接与个别的oracles交易。 聚合合约从多个oracles收集使用提交/显示方案提交的响应,计算查询的最终集合结果,
这种去中心化方法的主要挑战之一是汇总函数的制定。ChainLink建议计算加权响应,允许为每个oracle响应报告有效性分数。在这里 检测“无效”分数是非常重要的,因为它依赖于前提:由对等体提供的响应偏差测量的外围数据点是不正确的。基于响应分布中的oracle 响应的位置来计算有效性分数可能会使正确答案超过普通答案。因此,ChainLink提供了一组标准的聚合合约,但也允许指定自定义的 聚合合约。
一个相关的想法是SchellingCoin协议[11]。在这里,多个参与者报告价值,并将中位数作为“正确”答案。报告者必须提供重新分配的 存款,以支持更接近中位数的价值,从而激励报告与其他价值相似的价值。一个共同的价值,也称为Schelling Point,受访者可能 认为这是一个自然而明显的协调目标,预计将接近实际价值。
Teutsch最近提出了一种新的去中心化脱链数据可用性设计oracle [12]。该设计利用专用的工作证明区块链,该区块链能够正确地报告 在给定时期内的注册数据是否可用。矿工尝试下载,存储和传播所有当前注册的数据,因此保证数据在本地可用。虽然这样的系统在每个 挖掘节点存储和传播所有注册数据的意义上是昂贵的,但是系统允许通过在注册周期结束之后释放数据来重用存储。
# Solidity中的Oracle客户端接口
下面是一个Solidity示例,演示如何使用API从Oraclize连续轮询ETH/USD价格并以可用的方式存储结果。:
/*
ETH/USD price ticker leveraging CryptoCompare API
This contract keeps in storage an updated ETH/USD price,
which is updated every 10 minutes.
*/
pragma solidity ^0.4.1;
import "github.com/oraclize/ethereum-api/oraclizeAPI.sol";
/*
"oraclize_" prepended methods indicate inheritance from "usingOraclize"
*/
contract EthUsdPriceTicker is usingOraclize {
uint public ethUsd;
event newOraclizeQuery(string description);
event newCallbackResult(string result);
function EthUsdPriceTicker() payable {
// signals TLSN proof generation and storage on IPFS
oraclize_setProof(proofType_TLSNotary | proofStorage_IPFS);
// requests query
queryTicker();
}
function __callback(bytes32 _queryId, string _result, bytes _proof) public {
if (msg.sender != oraclize_cbAddress()) throw;
newCallbackResult(_result);
/*
* parse the result string into an unsigned integer for on-chain use
* uses inherited "parseInt" helper from "usingOraclize", allowing for
* a string result such as "123.45" to be converted to uint 12345
*/
ethUsd = parseInt(_result, 2);
// called from callback since we're polling the price
queryTicker();
}
function queryTicker() public payable {
if (oraclize_getPrice("URL") > this.balance) {
newOraclizeQuery("Oraclize query was NOT sent, please add some ETH to cover for the query fee");
} else {
newOraclizeQuery("Oraclize query was sent, standing by for the answer..");
// query params are (delay in seconds, datasource type, datasource argument)
// specifies JSONPath, to fetch specific portion of JSON API result
oraclize_query(60 * 10, "URL", "json(https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD,EUR,GBP).USD");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
要与Oraclize集成,合约EthUsdPriceTicker必须是usingOraclize的子项;usingOraclize合约在oraclizeAPI文件中定义。数据 请求是使用oraclize_query()函数生成的,该函数继承自usingOraclize合约。这是一个重载函数,至少需要两个参数:
- 支持的数据源,例如URL,WolframAlpha,IPFS或计算
- 给定数据源的参数,可能包括使用JSON或XML解析助手
价格查询在queryTicke()函数中执行。为了执行查询,Oraclize要求在以太网中支付少量费用,包括将结果传输和处理到__callback() 函数的gas成本以及随附的服务附加费。此数量取决于数据源,如果指定,则取决于所需的真实性证明类型。一旦检索到数据,__callback() 函数由Oraclize控制的帐户调用,该帐户被允许进行回调; 它传递响应值和唯一的queryId参数,作为示例,它可用于处理和跟踪来自 Oraclize的多个挂起的回调。
金融数据提供商Thomson Reuters还为以太坊提供了一项名为BlockOne IQ的oracle服务,允许在私有或许可网络上运行的智能合约请求 市场和参考数据[13]。下面是oracle的接口,以及将发出请求的客户端合约:
pragma solidity ^0.4.11;
contract Oracle {
uint256 public divisor;
function initRequest(uint256 queryType, function(uint256) external onSuccess, function(uint256) external onFailure) public returns (uint256 id);
function addArgumentToRequestUint(uint256 id, bytes32 name, uint256 arg) public;
function addArgumentToRequestString(uint256 id, bytes32 name, bytes32 arg) public;
function executeRequest(uint256 id) public;
function getResponseUint(uint256 id, bytes32 name) public constant returns(uint256);
function getResponseString(uint256 id, bytes32 name) public constant returns(bytes32);
function getResponseError(uint256 id) public constant returns(bytes32);
function deleteResponse(uint256 id) public constant;
}
contract OracleB1IQClient {
Oracle private oracle;
event LogError(bytes32 description);
function OracleB1IQClient(address addr) public payable {
oracle = Oracle(addr);
getIntraday("IBM", now);
}
function getIntraday(bytes32 ric, uint256 timestamp) public {
uint256 id = oracle.initRequest(0, this.handleSuccess, this.handleFailure);
oracle.addArgumentToRequestString(id, "symbol", ric);
oracle.addArgumentToRequestUint(id, "timestamp", timestamp);
oracle.executeRequest(id);
}
function handleSuccess(uint256 id) public {
assert(msg.sender == address(oracle));
bytes32 ric = oracle.getResponseString(id, "symbol");
uint256 open = oracle.getResponseUint(id, "open");
uint256 high = oracle.getResponseUint(id, "high");
uint256 low = oracle.getResponseUint(id, "low");
uint256 close = oracle.getResponseUint(id, "close");
uint256 bid = oracle.getResponseUint(id, "bid");
uint256 ask = oracle.getResponseUint(id, "ask");
uint256 timestamp = oracle.getResponseUint(id, "timestamp");
oracle.deleteResponse(id);
// Do something with the price data..
}
function handleFailure(uint256 id) public {
assert(msg.sender == address(oracle));
bytes32 error = oracle.getResponseError(id);
oracle.deleteResponse(id);
emit LogError(error);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
使用initRequest()函数启动数据请求,该函数除了两个回调函数之外,还允许指定查询类型(在此示例中,是对日内价格的请求)。 这将返回一个uint256标识符,然后可以使用该标识符提供其他参数。addArgumentToRequestString()函数用于指定RIC (Reuters Instrument Code),此处用于IBM股票,addArgumentToRequestUint()允许指定时间戳。现在,传入block.timestamp的 别名将检索IBM的当前价格。然后由executeRequest()函数执行该请求。处理完请求后,oracle合约将使用查询标识符调用onSuccess 回调函数,允许检索结果数据,否则在检索失败时使用错误代码进行onFailure回调。成功检索的可用字段包括开盘价,最高价,最低价, 收盘价(OHLC)和买/卖价。
Reality Keys [14]允许使用POST请求对事实进行离线请求。响应以加密方式签名,允许在链上进行验证。在这里,请求使用blockr.io API在特定时间检查比特币区块链上的账户余额:
wget -qO- https://www.realitykeys.com/api/v1/blockchain/new --post-data="chain=XBT&address=1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX&which_total=total_received&comparison=ge&value=1000&settlement_date=2015-09-23&objection_period_secs=604800&accept_terms_of_service=current&use_existing=1"
对于此示例,参数允许指定区块链,要查询的金额(总收到金额或最终余额)以及要与提供的值进行比较的结果,从而允许真或假的响应。 除了“signature_v2”字段之外,生成的JSON对象还包括返回值,该字段允许使用ecrecover()函数在智能合约中验证结果:
"machine_resolution_value" : "29665.80352",
"signature_v2" : {
"fact_hash" : "aadb3fa8e896e56bb13958947280047c0b4c3aa4ab8c07d41a744a79abf2926b",
"ethereum_address" : "6fde387af081c37d9ffa762b49d340e6ae213395",
"base_unit" : 1,
"signed_value" : "0000000000000000000000000000000000000000000000000000000000000001",
"sig_r" : "a2cd9dc040e393299b86b1c21cbb55141ef5ee868072427fc12e7cfaf8fd02d1",
"sig_s" : "8f3199b9c5696df34c5193afd0d690241291d251a5d7b5c660fa8fb310e76f80",
"sig_v" : 27
}
2
3
4
5
6
7
8
9
10
为了验证签名,ecrecover()可以确定数据确实由ethereum_address签名,如下所示。fact_hash和signed_value经过哈希处理,并将 三个签名参数传递给ecrecover():
bytes32 result_hash = sha3(fact_hash, signed_value);
address signer_address = ecrecover(result_hash, sig_v, sig_r, sig_s);
assert(signer_address == ethereum_address);
uint256 result = uint256(signed_value) / base_unit;
// Do something with the result..
2
3
4
5
# 参考
[1] http://www.oraclize.it/ (opens new window)
[2] https://tlsnotary.org/ (opens new window)
[3] https://tlsnotary.org/pagesigner.html (opens new window)
[4] https://bitcointalk.org/index.php?topic=301538.0 (opens new window)
[5] http://hackingdistributed.com/2017/06/15/town-crier/ (opens new window)
[6] https://www.cs.cornell.edu/~fanz/files/pubs/tc-ccs16-final.pdf (opens new window)
[7] https://www.crowdfundinsider.com/2018/04/131519-vitalik-buterin-outlines-off-chain-ethereum-smart-contract-activity-at-deconomy/ (opens new window)
[8] https://github.com/Azure/azure-blockchain-projects/blob/master/bletchley/EnterpriseSmartContracts.md (opens new window)
[9] https://people.cs.uchicago.edu/~teutsch/papers/truebit.pdf (opens new window)
[10] https://link.smartcontract.com/whitepaper (opens new window)
[11] https://blog.ethereum.org/2014/03/28/schellingcoin-a-minimal-trust-universal-data-feed/ (opens new window)
[12] http://people.cs.uchicago.edu/~teutsch/papers/decentralized_oracles.pdf (opens new window)
[13] https://developers.thomsonreuters.com/blockchain-apis/blockone-iq-ethereum (opens new window)
[14] https://www.realitykeys.com (opens new window)
# 其他链接
https://ethereum.stackexchange.com/questions/201/how-does-oraclize-handle-the-tlsnotary-secret (opens new window)
https://blog.oraclize.it/on-decentralization-of-blockchain-oracles-94fb78598e79 (opens new window)
https://medium.com/@YondonFu/off-chain-computation-solutions-for-ethereum-developers-507b23355b17 (opens new window)
https://blog.oraclize.it/overcoming-blockchain-limitations-bd50a4cfb233 (opens new window)
https://medium.com/@jeff.ethereum/optimising-the-ethereum-virtual-machine-58457e61ca15 (opens new window)
http://docs.oraclize.it/#ethereum (opens new window)
https://media.consensys.net/a-visit-to-the-oracle-de9097d38b2f (opens new window)
https://blog.ethereum.org/2014/07/22/ethereum-and-oracles/ (opens new window)
http://www.oraclize.it/papers/random_datasource-rev1.pdf (opens new window)
https://blog.oraclize.it/on-decentralization-of-blockchain-oracles-94fb78598e79 (opens new window)
https://www.reddit.com/r/ethereum/comments/73rgzu/is_solving_the_oracle_problem_a_paradox/ (opens new window)
https://medium.com/truebit/a-file-system-dilemma-2bd81a2cba25 (opens new window)
https://medium.com/@roman.brodetski/introducing-oracul-decentralized-oracle-data-feed-solution-for-ethereum-5cab1ca8bb64 (opens new window)
# 第十三章 燃气(Gas)
Gas是以太坊用于衡量程序执行一个或一组动作所需计算量的单位。交易或合约执行的每项操作都需要一定数量的gas; 所需的gas数量 与正在执行的计算步骤的类型和数量有关。与仅以千字节(kB)计算交易规模的比特币交易费相比,以太坊交易费必须考虑智能合约代码 可以执行的任意数量的计算步骤。程序执行的操作数越多,运行完成的成本就越高。
每次操作都需要固定量的gas。以太坊黄皮书的的一些例子:
- 添加两个数字需要3个gas
- 计算Keccak256哈希值,需要30个gas+ 每256位数据被哈希6个gas
- 发送交易成本为21000 gas
gas是以太坊的重要组成部分,具有双重作用。一,作为以太坊价格(具有波动性)和矿工对其工作的奖励之间的抽象层。另一种是抵御拒绝 服务攻击。为了防止网络中的意外或恶意无限循环或其他计算浪费,每个交易的发起者需要设置他们愿意花费在gas上的金额的限制。因此, gas系统阻止攻击者发送垃圾邮件交易,因为他们必须按比例支付他们消耗的计算,带宽和存储资源。
# 停机问题
交易费和会计的想法似乎很实用,但你可能想知道为什么以太坊首先需要gas。gas是关键,因为它不仅可以解决停机问题,而且对安全和活 力也至关重要。什么是停机问题,安全和活力,为什么要关心?
# 支付gas
虽然gas有价格,但它不能“拥有”也不能“花”。gas仅存在于以太坊虚拟机(EVM)内部,作为计算工作量的计数。发起方被收取ether交 易费,然后转换为gas,然后转回到ether,作为矿工的块奖励。这些转换步骤用于将计算的价格(与“工作量”相关)与ether的价格(与 市场波动相关)分开。
# gas成本与gas价格
虽然gas成本是EVM中执行的操作步骤的度量,但gas本身也具有以ether测量的gas价格。在执行交易时,发起方指定他们愿意为 每个gas单位支付的gas价格(以ether为单位),允许市场决定ether的价格与计算操作的成本之间的关系(以gas衡量) 。
total gas used * gas price paid = transaction fee
,以ether为单位
此金额将在交易执行开始时从发起方的帐户中扣除。发起方不是设置“total gas used”,而是设置gas limit,该限制应足以覆盖执行 交易所需的gas量。
# gas成本限制和gas耗尽
在发送交易之前,发起方必须指定gas limit - 他们愿意购买的最大gas数量。他们还必须指定gas price - 他们愿意为每单位 gas支付的以太价格。
以ether计算的gas limit * gas price
在交易执行开始时从发起方的账户中扣除作为存款。这是为了防止发送者在执行中期“破产”并且
无法支付gas费用。由于这个原因,用户也无法设置超出其帐户余额的gas限制。
理想情况下,发起方将设置一个高于或等于实际使用的gas的gas限制。如果gas限制设置高于消耗的gas量,发货人将收到超额金额的退款, 因为矿工只获得他们实际工作的补偿。
在这种情况下:
(gas限制 - 多余gas)*gas
价格以太以矿块作为块奖励
(gas limit - excess gas) * gas price
ether作为矿工的区块奖励
excess gas * gas price
ether退回发起方
但是,如果使用的gas超过规定的gas限制,即如果在执行期间交易“runs out of gas”,则操作终止。虽然交易不成功,但由于矿工已经 完成了计算工作,不会退回发送人的交易费用,矿工因此得到补偿。
# 示例
如果交易是从外部拥有账户(EOA)发送的,则从EOA的余额中扣除gas费。换句话说,交易的发起人正在支付gas费。发起人为交易消耗的 总gas以及随后的任何子执行提供资金。这意味着如果发起者X附加1000个gas来调用合约A,其在计算上花费500个gas然后向合约B发送另 一个消息,则A用于将消息发送到B的gas也会再已开始从X的gas限制中扣除。
EOA帐户X启动交易并调用合约帐户A上的功能,附带1000个gas
合约A在计算上花费500gas,并向合约B发送消耗100gas的消息
合约B在计算上花费300个gas并完成交易。
100个gas退还给X.
2
3
4
5
6
7
因此,如果该交易的发起人在开始时没有附加足够高的gas费,那么在交易中执行一部分操作的中间合约(例如,在我们的示例中为合约A) 理论上可以耗尽gas。如果合约在执行中期用完,除了gas费支付外,所有状态变更都会被撤销。
# 估算 Gas
通过假装交易已经被包含在区块链中来估算gas,然后返回如果操作是真实的那么可以收取的确切gas量。换句话说,它使用与矿工用来计算 实际费用但从未开采过区块链的完全相同的程序。
请注意,由于各种原因(包括EVM机制和节点性能),估计值可能远远超过交易实际使用的gas量。
var result = web3.eth.estimateGas({
to: "0xc4abd0339eb8d57087278718986382264244252f",
data: "0xc6888fa10000000000000000000000000000000000000000000000000000000000000003"
});
console.log(result); // "0x0000000000000000000000000000000000000000000000000000000000000015"
2
3
4
5
# gas价格和交易优先顺序
gas价格是交易发起方愿意为每个gas单位支付的ether量。开采下一个区块的矿工决定要包括哪些交易。由于gas价格被计入他们将作为奖励 的交易费中,他们更可能首先包括具有最高gas价格的交易。如果发起方将gas价格设置得太低,他们可能需要等待很长时间才能将其交易进 入一个区块。
矿工还可以决定块中包含交易的顺序。由于多个矿工竞争将其区块附加到区块链,因此区块内的交易顺序由“获胜”矿工任意决定,然后其他 矿工用该订单核实。虽然可以任意排列来自不同账户的交易,但是来自个人账户的交易是按照自动递增的随机数的顺序执行的。
# 区块gas限制
区块gas限制是区块中允许的最大gas量,用于确定区块中可以容纳的交易数量。例如,假设我们有5个交易,其中每个交易的gas限制为10, 20,30,40和50.如果区块gas限制为100,那么前四个交易可以适合该区块,而交易5必须等待未来的区块。如前所述,矿工决定在一个区块中 包含哪些交易。不同的矿工可以尝试包括块中的最后2个交易(50 + 40),并且它们仅具有包括第一个交易(10)的空间。如果你尝试包含 的交易需要的gas量超过当前的gas限制,则网络将拒绝该交易,你的以太坊客户将向你发送消息“交易超过区块gas限制。根据 https://etherscan.io (opens new window)的数据,目前区块的gas限制在500万左右。即一个区块可以容纳约238个交易,每个 交易消耗21000个gas。
# 谁来决定区块gas限制是多少?
网络上的矿工决定区块gas限制是什么。想要在以太坊网络上挖矿的个人使用挖矿程序,例如ethminer,它连接到Geth或Parity Ethereum 客户端。以太坊协议有一个内置机制,矿工可以对gas限制进行投票,因此无需在硬分叉上进行协调就可以增加容量。块的矿工能够在任一方 向上将块气限制调整1/1024(0.0976%)。其结果是根据当时网络的需要调整块大小。这一机制与默认的开采策略结合在一起,矿工默认将 投票决定至少470万的gas限制,但如果这一数字更高的话,将把目标对准最近的(1024区块指数移动)平均gas的150%,从而使数量有机地增 加。矿工们可以选择改变这一点,但是他们中的许多人不这样做,并保留默认值。
# gas退款
以太坊通过退还高达一半的gas费用来鼓励删除存储的变量。 EVM中有2个负的gas操作:
清理合约是-24,000(SELFDESTRUCT) 清理存储为-15,000(SSTORE [x] = 0)
# GasToken
GasToken是一种符合ERC20标准的token,允许任何人在gas价格低时“储存”gas,并在gas价格高时使用gas。通过使其成为可交易的资产, 它基本上创造了一个gas市场。 它的工作原理是利用前面描述的gas退款机制。
你可以在https://gastoken.io/ (opens new window)了解计算盈利能力以及如何使用释放gas所涉及的数学
# 租金
目前,以太坊社区提出了一项关于向智能合约收取“租金”以保持活力的建议。
在没有支付租金的情况下,智能合约将被“睡眠”,即使是简单的读取操作也无法获得数据。需要通过支付租金和提交Merkle证据来唤醒进入 睡眠状态的合约。
https://github.com/ethereum/EIPs/issues/35 (opens new window)
https://ethresear.ch/t/a-simple-and-principled-way-to-compute-rent-fees/1455 (opens new window)
https://ethresear.ch/t/improving-the-ux-of-rent-with-a-sleeping-waking-mechanism/1480 (opens new window)
# 第十四章 以太坊虚拟机
# 这是什么?
实际处理内部状态和计算的协议部分称为以太坊虚拟机(EVM)。从实际角度来看,EVM可以被认为是包含数百万个对象的大型去中心化计 算机。
# 比较
虚拟机(Virtual Machine)(Virtualbox, QEMU, 云计算)
Java 虚拟机(VM)
虚拟机技术(如Virtualbox和QEMU / KVM)与EVM的不同之处在于它们的目的是提供管理程序功能,或者处理客户操作系统与底层主机操作 系统和硬件之间的系统调用,任务调度和资源管理的软件抽象。
然而,Java VM(JVM)规范的某些方面确实包含与EVM的相似之处。从高级概述来看,JVM旨在提供与底层主机操作系统或硬件无关的运行时 环境,从而实现各种系统的兼容性。在JVM上运行的高级程序语言(如Java或Scala)被编译到相应的指令集字节码中。这与编译要在EVM上 运行的Solidity源文件相当。
# EVM机器语言(字节码操作)
EVM机器语言分为特定的指令集组,例如算术运算,逻辑和比较运算,控制流,系统调用,堆栈操作和存储器操作。除典型的字节码操作外, EVM还必须管理账户信息(即地址和余额),当前gas价格和区块信息。
通用堆栈操作:: 堆栈和内存管理的操作码指令:
POP // 项目出栈
PUSH // 项目入栈
MLOAD // 将项目加载到内存中
MSTORE // 在内存中存储项目
JUMP // 改变程序计数器的位置
PC // 程序计数器
MSIZE // 活动的内存大小
GAS // 交易可用的gas数量
DUP // 复制栈项目
SWAP // 交换栈项目
2
3
4
5
6
7
8
9
10
通用系统操作:: 执行程序的系统的操作码指令:
CREATE // 创建新的账户
CALL // 在账户间传递消息的指令
RETURN // 执行停机
REVERT // 执行停机,恢复状态更改
SELFDESTRUCT // 执行停机,并标记账户为删除的
2
3
4
5
算术运算:: 通用算术运算代码指令:
添加//添加
MUL //乘法
SUB //减法
DIV //整数除法
SDIV //有符号整数除法
MOD // Modulo(剩余)操作
SMOD //签名模运算
ADDMOD //模数加法
MULMOD //模数乘法
EXP //指数运算
STOP //停止操作
2
3
4
5
6
7
8
9
10
11
环境操作码:: 处理执行环境信息的通用操作码:
ADDRESS //当前执行账户的地址
BALANCE //账户余额
CALLVALUE //执行环境的交易值
ORIGIN //执行环境的原始地址
CALLER //执行调用者的地址
CODESIZE //执行环境代码大小
GASPRICE //gas价格状态
EXTCODESIZE //账户的代码大小
RETURNDATACOPY //从先前的内存调用输出的数据的副本
2
3
4
5
6
7
8
9
# 状态
与任何计算系统一样,状态概念也很重要。就像CPU跟踪执行过程一样,EVM必须跟踪各种组件的状态以支持交易。这些组件的状态最终会推动 总体区块链的变化程度。这方面导致将以太坊描述为_基于交易的状态机_,包含以下组件:
World State:: 160位地址标识符和账户状态之间的映射,在不可变的_Merkle Patricia Tree_数据结构中维护。
Account State:: 包含以下四个组件:
nonce:表示从该相应账户发送的交易数量的值。
balance:账户地址拥有的_wei_的数量。
storageRoot:Merkle Patricia Tree根节点的256位哈希值。
codeHash:各个账户的EVM代码的不可变哈希值。
Storage State:: 在EVM上运行时维护的账户特定状态信息。
Block State:: 交易所需的状态值包括以下内容:
blockhash:最近完成的块的哈希值。
coinbase:收件人的地址。
timestamp:当前块的时间戳。
number:当前块的编号。
difficulty:当前区块的难度。
gaslimit:当前区块的gas限制。
Runtime Environment Information:: 用于使用交易的信息。
gasprice:当前汽油价格,由交易发起人指定。
codesize:交易代码库的大小。
caller:执行当前交易的账户的地址。
origin:当前交易原始发件人的地址。
状态转换使用以下函数计算:
以太坊状态转换函数:: 用于计算 valid state transition 。
区块终结状态转换函数:: 用于确定最终块的状态,作为挖矿过程的一部分,包含区块奖励。
区块级状态转换函数:: 应用于交易状态时的区块终结状态转换函数的结果状态。
# 将Solidity编译为EVM字节码
可以通过命令行完成将Solidity源文件编译为EVM字节码。有关其他编译选项的列表,只需运行以下命令:
$ solc --help
使用 --opcodes 命令行选项可以轻松实现生成Solidity源文件的原始操作码流。此操作码流会遗漏一些信息(_--asm_选项会生成完整信息), 但这对于第一次介绍是足够的。例如,编译示例Solidity文件 Example.sol 并将操作码输出填充到名为 BytecodeDir 的目录中,使用以 下命令完成:
$ solc -o BytecodeOutputDir --opcodes Example.sol
或
$ solc -o BytecodeOutputDir --asm Example.sol
以下命令将为我们的示例程序生成字节码二进制文件:
$ solc -o BytecodeOutputDir --bin Example.sol
生成的输出操作码文件将取决于Solidity源文件中包含的特定合约。我们的简单Solidity文件 Example.sol
[simple_solidity_example]只有一个名为“example”的合约。
pragma solidity ^0.4.19;
contract example {
address contractOwner;
function example() {
contractOwner = msg.sender;
}
}
2
3
4
5
6
7
8
9
10
如果查看 BytecodeDir 目录,你将看到操作码文件 example.opcode (请参阅[simple_solidity_example]),其中包含“example” 合约的EVM机器语言操作码指令。在文本编辑器中打开 example.opcode 文件将显示以下内容:
PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH1 0xE JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST CALLER PUSH1 0x0 DUP1 PUSH2 0x100 EXP DUP2 SLOAD DUP2 PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF MUL NOT AND SWAP1 DUP4 PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND MUL OR SWAP1 SSTORE POP PUSH1 0x35 DUP1 PUSH1 0x5B PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x60 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 REVERT STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 JUMP 0xb9 SWAP14 0xcb 0x1e 0xdd RETURNDATACOPY 0xec 0xe0 0x1f 0x27 0xc9 PUSH5 0x9C5ABCC14A NUMBER 0x5e INVALID EXTCODESIZE 0xdb 0xcf EXTCODESIZE 0x27 EXTCODESIZE 0xe2 0xb8 SWAP10 0xed 0x
使用 --asm 选项编译示例会在 BytecodeDir 目录中生成一个文件 example.evm。这包含详细的EVM机器语言说明:
/* "Example.sol":26:132 contract example {... */
mstore(0x40, 0x60)
/* "Example.sol":74:130 function example() {... */
jumpi(tag_1, iszero(callvalue))
0x0
dup1
revert
tag_1:
/* "Example.sol":115:125 msg.sender */
caller
/* "Example.sol":99:112 contractOwner */
0x0
dup1
/* "Example.sol":99:125 contractOwner = msg.sender */
0x100
exp
dup2
sload
dup2
0xffffffffffffffffffffffffffffffffffffffff
mul
not
and
swap1
dup4
0xffffffffffffffffffffffffffffffffffffffff
and
mul
or
swap1
sstore
pop
/* "Example.sol":26:132 contract example {... */
dataSize(sub_0)
dup1
dataOffset(sub_0)
0x0
codecopy
0x0
return
stop
sub_0: assembly {
/* "Example.sol":26:132 contract example {... */
mstore(0x40, 0x60)
0x0
dup1
revert
auxdata: 0xa165627a7a7230582056b99dcb1edd3eece01f27c9649c5abcc14a435efe3bdbcf3b273be2b899eda90029
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
--bin 选项产生以下内容:
60606040523415600e57600080fd5b336000806101000a81548173
ffffffffffffffffffffffffffffffffffffffff
021916908373
ffffffffffffffffffffffffffffffffffffffff
160217905550603580605b6000396000f3006060604052600080fd00a165627a7a7230582056b99dcb1e
2
3
4
5
让我们检查前两条指令(参考[common_stack_opcodes]):
PUSH1 0x60 PUSH1 0x40
这里我们有_mnemonic_“PUSH1”,后跟一个值为“0x60”的原始字节。这对应于EVM指令,该操作将操作码之后的单字节解释为文字值并将 其推入堆栈。可以将大小最多为32个字节的值压入堆栈。例如,以下字节码将4字节值压入堆栈:
PUSH4 0x7f1baa12
第二个push操作码将“0x40”存储到堆栈中(在那里已存在的“0x60”之上)。
接下来的两个指令:
MSTORE CALLVALUE
MSTORE是一个堆栈/内存操作(参见[common_stack_opcodes]),它将值保存到内存中,而CALLVALUE是一个环境操作码 (参见[common_environment_opcodes]),它返回正在执行的消息调用的存放值。
# 执行EVM字节码
# Gas,会计
对于每个交易,都有一个关联的 gas-limit 和 gas-price ,它们构成了EVM执行的费用。这些费用用于促进交易的必要资源,例如 计算和存储。gas还用于创建账户和智能合约。
# 图灵完备性和gas
简单来说,如果系统或编程语言可以解决你输入的任何问题,它是 图灵完备的 。这在以太坊黄皮书中讨论过:
[quote, Gavin Wood, ETHEREUM: A SECURE DECENTRALISED GENERALISED TRANSACTION LEDGER]
It is a quasi-Turing complete machine; the quasi qualification comes from the fact that the computation is intrinsically bounded through a parameter, gas, which limits the total amount of computation done.
虽然EVM理论上可以解决它收到的任何问题,但gas可能会阻止它这样做。这可能在以下几个方面发生:
1)在以太坊开采的块具有与之相关的gas限制; 也就是说,区块内所有交易所使用的总gas不能超过一定限度。 2)由于gas和gas价格齐头并进,即使取消了gas限制,高度复杂的交易也可能在经济上不可行。
但是,对于大多数用例,EVM可以解决提供给它的任何问题。
# 字节码与运行时字节码
编译合约时,你可以获得 合约字节码 或 运行时字节码 。
合约字节码包含实际上最终位于区块链上的字节码 以及 将字节码放在区块链上并运行合约构造函数所需的字节码。
另一方面,运行时字节码只是最终位于区块链上的字节码。这不包括初始化合约并将其放在区块链上所需的字节码。
让我们以前面创建的简单Faucet.sol
合约为例。
// Version of Solidity compiler this program was written for
pragma solidity ^0.4.19;
// 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 {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
要获得合约字节码,我们将运行solc --bin Faucet.sol
。如果我们只想要运行时字节码,我们将运行
solc --bin-runtime Faucet.sol
。
如果比较这些命令的输出,你将看到运行时字节码是合约字节码的子集。换句话说,运行时字节码完全包含在合约字节码中。
# 反汇编字节码
反汇编EVM字节码是了解高级别Solidity在EVM中的作用的好方法。你可以使用一些反汇编程序来执行此操作:
- Porosity 是一个流行的开源反编译器:https://github.com/comaeio/porosity (opens new window)
- Ethersplay 是Binary Ninja的EVM插件,一个反汇编程序:https://github.com/trailofbits/ethersplay (opens new window)
- IDA-Evm 是IDA的EVM插件,另一个反汇编程序:https://github.com/trailofbits/ida-evm (opens new window)
在本节中,我们将使用 Binary Ninja 的 Ethersplay 插件。
在获取Faucet.sol的运行时字节码后,我们可以将其提供给Binary Ninja(在导入Ethersplay插件之后)以查看EVM指令。

Figure 1. Disassembling the Faucet runtime bytecode
当你将交易发送到智能合约时,交易首先会与该智能合约的 调度员(dispatcher) 进行交互。调度程序读入交易的数据字段并将其 发送到适当的函数。
在熟悉的MSTORE指令之后,我们在编译的Faucet.sol合约中看到以下创建:
PUSH1 0x4
CALLDATASIZE
LT
PUSH1 0x3f
JUMPI
2
3
4
5
"PUSH1 0x4" 将0x4置于堆栈顶部,栈初始为空。“CALLDATASIZE”获取接收到的交易的calldata的大小(以字节为单位)并将其推送到 堆栈中。当前堆栈如下所示:
Table 1. Current stack
Stack |
0x4 |
length of calldata from tx (msg.data) |
下一条指令是“LT”,是“小于(less than)”的缩写。LT指令检查堆栈上的顶部项是否小于堆栈上的下一项。在我们的例子中,它检查 CALLDATASIZE的结果是否小于4个字节。
为什么EVM会检查交易的calldata是否至少为4个字节?因为函数标识符的工作原理。每个函数由其keccak256哈希的前四个字节标识。 通过将函数的名称和它所采用的参数放入keccak256哈希函数,我们可以推导出它的函数标识符。在我们的合约中,我们有:
keccak256("withdraw(uint256)") = 0x2e1a7d4d...
因此,“withdraw(uint256)”函数的函数标识符是0x2e1a7d4d,因为它们是结果哈希的前四个字节。函数标识符总是4个字节长,所以 如果发送给合约的交易的整个数据字段小于4个字节,那么除非定义了 fallback函数,否则没有交易可能与之通信的函数。因为我们在 Faucet.sol中实现了这样的fallback函数,所以当calldata的长度小于4个字节时,EVM会跳转到此函数。
如果msg.data字段少于4个字节,LT将弹出堆栈的前两个值并将1推到其上。否则,它会推入0。在我们的例子中,让我们假设发送给我们的 合约的transaciton的msg.data字段 was 少于4个字节。
“PUSH1 0x3f”指令将字节“0x3f”压入堆栈。在此指令之后,堆栈如下所示:
Table 2. Current stack
Stack |
1 |
0x3f |
下一条指令是“JUMPI”,代表“jump if”。它的工作原理如下:
jumpi(label, cond) // Jump to "label" if "cond" is true
在我们的例子中,“label”是0x3f,这是我们的fallback函数存在于我们的智能合约中的地方。“cond”参数为1,它来自之前LT指令的 结果。要将整个序列放入单词中,如果交易数据少于4个字节,则合约将跳转到fallback函数。

Figure 2. JUMPI instruction leading to fallback function
我们来看一下调度员的核心代码块。假设我们收到的长度大于4个字节的calldata,“JUMPI”指令不会跳转到回退函数。相反,代码执行将 遵循下一条指令:
PUSH1 0x0
CALLDATALOAD
PUSH29 0x1000000...
SWAP1
DIV
PUSH4 0xffffffff
AND
DUP1
PUSH4 0x2e1a7d4d
EQ
PUSH1 0x41
JUMPI
2
3
4
5
6
7
8
9
10
11
12
“PUSH1 0x0”将0压入堆栈,否则为空。“CALLDATALOAD”接受发送到智能合约的calldata中的索引作为参数,并从该索引读取32个字节, 如下所示:
calldataload(p) // call data starting from position p (32 bytes)
由于0是从PUSH1 0x0命令传递给它的索引,因此CALLDATALOAD从字节0开始读取32字节的calldata,然后将其推送到堆栈的顶部(在弹出 原始0x0之后)。在“PUSH29 0x1000000 ...”指令之后,堆栈如下所示:
Table 3. Current stack
Stack |
32 bytes of calldata starting at byte 0 |
0x1000000… (29 bytes in length) |
“SWAP1”用它后面的_第i个_元素交换堆栈顶部元素。在这里,它与密钥数据交换0x1000000 ... 新堆栈如下所示:
Table 4. Current stack
Stack |
0x1000000… (29 bytes in length) |
32 bytes of calldata starting at byte 0 |
下一条指令是“DIV”,其工作方式如下:
div(x, y) // x / y
在这里,x = 32字节的calldata从字节0开始,y = 0x100000000 ...(总共29个字节)。你能想到调度员为什么要进行划分吗?这是 一个提示:我们从索引0开始从calldata读取32个字节。该calldata的前四个字节是函数标识符。
我们之前推送的0x100000000 ....长度为29个字节,由开头的1组成,后跟全0。将我们的32字节的calldata除以此0x100000000 .... 将只留下从索引0开始的callataload的_topmost 4字节_这四个字节 - 从索引0开始的calldataload中的前四个字节 - 是函数标识符, 并且这就是EVM如何提取该字段。
如果你不清楚这一部分,可以这样想:在base~10~,1234000/1000 = 1234。在base~16~中,这没有什么不同。不是每个地方都是10的 倍数,它是16的倍数。正如在我们的较小的例子中除以10^3^(1000)只保留最顶部的数字,将我们的32字节基数~16~值除以16^29^做同 样的事。
DIV(函数标识符)的结果被推送到堆栈上,我们的新堆栈如下:
Table 5. Current stack
Stack |
function identifier sent in msg.data |
由于“PUSH4 0xffffffff”和“AND”指令是冗余的,我们可以完全忽略它们,因为堆栈在完成后将保持不变。“DUP1”指令复制堆栈上的 项,这是函数标识符。下一条指令“PUSH4 0x2e1a7d4d”将抽取(uint256)函数的计算函数标识符推送到堆栈。堆栈现在 看起来如下:
Table 6. Current stack
Stack |
function identifier sent in msg.data |
function identifier sent in msg.data |
0x2e1a7d4d |
下一条指令“EQ”弹出堆栈的前两项并对它们进行比较。这是调度程序完成其主要工作的地方:它比较交易的msg.data字段中发送的函数 标识符是否与withdraw(uint256)匹配。如果它们相等,则EQ将1推入堆栈,这最终将用于跳转到fallback函数。否则,EQ将0推入 堆栈。
假设发送给我们合约的交易确实以withdraw(uint256)的函数标识符开头,我们的新栈看起来如下:
Table 7. Current stack
Stack |
function identifier sent in msg.data |
1 |
接下来,我们有“PUSH1 0x41”,这是withdraw(uint256)函数在合约中的地址。在此指令之后,堆栈如下所示:
Table 8. Current stack
Stack |
function identifier sent in msg.data |
1 |
0x41 |
接下来是JUMPI指令,它再次接受堆栈上的前两个元素作为参数。在这种情况下,我们有“jumpi(0x41,1)”,它告诉EVM执行跳转到 withdraw(uint256)函数的位置。
# EVM工具参考
- ByteCode To Opcode Disassembler (opens new window) (用于检查/调试编译是否完整运行,如果源代码 未发布则可用于逆向工程)
# 第十五章 共识
以太坊网络中的共识是指多个节点或代理在给定的时间点就区块链状态达成一致的能力。这与传统的定义为个人或群体之间的一般协议的 共识密切相关但不同。在这里,社区必须解决在技术上(在网络内)和社交上达成共识的挑战(以确保协议不会分叉或破裂)。本章将概 述建立共识的一些技术方法。
当涉及区块链上分散记录保存和验证的核心功能时,单独依靠信任来确保添加到账本的信息是正确的可能会成为问题。这种挑战在去中心化 网络中更为明显,因为没有中央实体来决定什么应该和不应该被视为是真实的。缺乏一个中央决策实体是区块链受欢迎程度的主要吸引力之一, 因为系统能够抵抗审查制度,并且无需对许可或信息获取权限的依赖。然而,这些好处可能带来成本,因为如果没有可信的仲裁员,任何分 歧,欺骗或差异都需要使用数学,经济或社会技术进行协调。因此,分散的系统更有抵御攻击的能力,但在应对变化时却不那么果断。
获得共识和信任信息的能力将对区块链技术作为资产类别和技术的未来采用和实用具有重要意义。为了应对这一挑战并保持权力下放的重要 性,社区不断尝试不同共识模式,我们将在本章中探讨。
# 共识度量
共识度量是可测量的数据,区块链网络的节点必须在该数据上达成一致,以便为每个块中包含的数据建立并保持一致。在区块链技术中,每次 将新块添加到链中时,每个网络节点都会测量并批准一致性度量。作为共识度量的结果,区块链充当了从一个确定可验证的事实延伸到下一个 事实的真理链。由于共识度量,区块链协议的节点变为 迷你公证人 mini-notaries,能够从真实的节点中立即分辨出区块链的错误 副本,并将该事实报告给整个网络。这些措施是必需的,以便阻止通过提交包含虚假信息的区块来欺骗网络不良行为者。由于一致性度量, 区块链不仅建立了的完整性,而且长期保持不变。共识度量有多种形式,但对于此讨论而言,最重要的两种是基于风险的度量和基于工作量 的度量。
# 基于Hash的度量
通常称为工作量证明(PoW)度量,这些度量建立了共识,因为使用它们的协议将计算机设置为查找难题的答案。找到适合网络参数的散列 的难题要求节点提交处理能力并使用电力与其他节点竞争以提出有效的哈希。为了便于说明,可以考虑超级计算机,它的唯一工作就是在 整数空间中搜索素数。现在考虑由普通计算机组成的整个网络。这些计算机放在一起时,可以说具有超级计算机的组合计算能力。这个 计算机网络的唯一工作类似于搜索另一种称为SHA-256哈希的数字的可能数字。这些数字具有独特的属性,就像素数一样,尽管在生成 符合网络设定标准的哈希方面存在很大困难,但在发现时可以轻松验证它们。想象计算哈希和验证哈希的一种方法是使用拼图游戏类比。 这个难题非常困难且耗时,但是一眼就能看出它是否已经完成。
当准确计算SHA-256哈希值时,它们可用作证明已使用一定量的计算能力来查找数字的证明。最新的素数是asciimath: [2^(77,232,917)- 1]。它是由计算机发现的,就像计算机发现的SHA-256哈希一样。SHA-256哈希比新素数更容易找到,但是找到 哈希的固有难点在于基于哈希的度量得出它们的能力。
每个SHA-256哈希都有64个十六进制字符。例如,这里是单词“yank”的SHA-256哈希。
“yank”(SHA-256)= 45F1B9FC8FD5F760A2134289579DF920AA55830F2A23DCF50D34E16C1292D7E0
将其与三个字母“yan”的SHA-256哈希:
“yan”(SHA-256)= 281ACA1A80B52620BD717E8B14A0386B8ADA92AE859AC2C8A2AC222EFA02EDBB
两个字母“ya”的SHA-256哈希:
“ya”(SHA-256)= 6663103A3E47EFC879EA31FA38458BE23BE0CE0895F3D8B27B7EA19A1120B3D4
单个字母“y”的SHA-256哈希:
“y”(SHA-256)= A1FCE4363854FF888CFF4B8E7875D600C2682390412A8CF79B37D0B11148B0FA
# 基于哈希的度量验证
如果你对足够多的随机短语进行了哈希,有点像打字机上的猴子,最终你会发现一个匹配特定模式的哈希。在哈希为“ya”的情况下,请注意 它以模式“666”开头。这类似于比特币,但比特币要求找到与以“000”开头的模式匹配的哈希值。可以通过将前一个区块中的信息插入到 SHA-256哈希算法中来创建的任何哈希,并用于创建下一个块,只要它在数字中具有正确数量的前导零,网络就会认可,并且区块奖励将 是你的。
由于想要挖掘比特币的人数不断增加,因此每秒寻找SHA-256哈希值的算力总是越来越多。比特币软件通过自动调整共识度量的难度来处理 这种意外事件,增加前导零的数量以形成共识。这可以保证新块的创建时间与前面的块大致相同。对于比特币网络,为十分钟,但可以轻松 更改。在以太坊中,平均区块生成时间约为10秒。
# 基于风险的度量
通常称为Proof-of-Stake(PoS)度量,这些度量基于以下事实建立共识:选择创建无效区块的每个人都会失去比通过创建有效区块获得的 更多的东西。该度量是通过关于链内数据的共识创造的,而不是关于链外数据的共识。基于哈希的共识度量主要关注SHA-256哈希的质量和精 确性。基于风险的度量主要关注任何特定节点在添加新块时所承担的风险。
所有节点在这里达成一致的度量是哪些节点创建了正确的块,哪些节点没有。在某种程度上,这已经内置在比特币协议中。比特币协议假定 正确的块是大多数节点正在挖掘的块。它假设矿工不会选择不正确的区块,因为这不利于挖掘坏区块。
另一方面,基于风险的链依赖于对未能创建网络中其他节点认可的块的创建者的快速,即时和不可逆转的影响。通过强制执行资源丢失的 风险,基于哈希的度量(也依赖于不想浪费资源的人员)过程可以以更简单的方式进行缩短和实施。目前正在研究在以太坊中实现这种 共识度量模型。
工作量证明是一种共识协议,它将网络中的有效区块链视为创建计算成本最高的链。这里提到的计算工作是将所有块添加到当前区块链 所必须完成的工作。这项工作由网络节点完成,并且必须在计算上很困难,以便使工作变得非常重要,但也必须是可行的,以便在经过 合理的努力之后可以实现。最终,网络将依赖于提供此PoW的节点以维持区块链,因此,网络的最佳利益是需要合理的PoW。
在以太坊网络以及许多其他区块链网络中,获取PoW需要找到要添加到区块链的块的哈希。这个哈希是通过散列由块的数据和随机数组成的 字符串获得的(创建此字符串的方法可能会有所不同,但整个过程是相同的)。该哈希必须小于某个阈值(由网络的难度确定),并且一旦 节点发现产生该哈希的随机数,则接受相应的块并将其添加到区块链中。
找到这个有效散列的方法是修改nonce,通常将其初始化为零并在每次迭代时递增,直到产生低于网络阈值的哈希。此过程称为挖掘。由于 挖掘中使用的哈希函数的性质,找到有效随机数的唯一方法是通过暴力搜索,即检查随机数的每个可能值,直到找到满足网络要求的散列。 因此,提供有效的随机数被认为是PoW。
# PoS
权益证明(PoS,Proof-of-Stake)是公共区块链的一类共识算法,它依赖于验证者在网络中的经济利益。在基于工作量证明(PoW)的 公共区块链(例如比特币和以太坊的当前实现)中,该算法奖励解密加密谜题的参与者,以便验证交易并创建新的块(即挖掘)。在基于 PoS的公共区块链中(例如以太坊即将发布的Casper实现),一组验证者轮流对下一个块进行建议和投票,每个验证者的投票权重取决于 其存款的大小(即赌注)。PoS的显着优势包括安全性,降低集中风险和能源效率。
通常,权益证明算法如下。区块链跟踪一组验证者,任何持有区块链基本加密货币的人(在以太坊的情况下是ether)都可以通过发送一种 特殊类型的交易来将其以太币锁定到存款中,从而成为验证者。然后,通过所有当前验证者都可以参与的一致性算法来完成创建和同意新块 的过程。
有许多种共识算法,以及许多方法可以为参与共识算法的验证人分配奖励,因此有许多“口味”的权益证明。从算法的角度来看,有两种主要 类型:基于链的权益证明和BFT风格的权益证明。
在基于链的证明中,算法在每个时隙中伪随机地选择一个验证者(例如,每个10秒的周期可能是一个时隙),并为该验证者分配创建单 个块的权限,这个块必须指向一些前一个块(通常是前一个最长链末端的块),因此随着时间的推移,大多数块会聚成一个不断增长的链。
在BFT风格的股权证明中,验证者被随机分配提出区块的权利,但是通过多轮过程来确定哪个区块是规范的,其中每个验证者在每轮中发 送对某个特定区块的“投票”,在流程结束时,所有(诚实和在线)验证者永久同意任何给定的块是否属于链条的一部分。请注意,块可能 仍然链接在一起; 关键的区别在于块上的共识可以在一个块内,并且不依赖于它之后的链的长度或大小。
# PoA
授权证明(PoA)是PoS一致性算法的子集,主要由测试网和私有或联盟网络使用。在基于PoA的区块链中,交易有效性最终由一组经批准的 链上账户确定,称为“授权节点”。确定授权节点的标准是通过网络治理结构中编写的方法确定性地决定的。
PoA被广泛认为是达成共识的最快途径,但依赖于验证节点尚未受到损害的假设。非验证参与者可以像公共以太网那样访问和使用网络(通过 利用p2p交易,合约,账户等)
PoA共识依赖于验证者的声誉和过去的表现。这个想法是验证者节点将其身份/声誉放到我的身上。私人联盟网络的一个重要方面是链上地址 与已知的现实世界身份之间的联系。因此,我们可以说验证节点正在盯着他们的“身份”或“声誉”(而不是他们的经济持有)。这为验证者 创建了一定程度的问责制,最适合企业,私有或测试网络。
PoA目前由测试网络Kovan(PoA网络)使用,并且可以在Parity中轻松配置用于私人联盟网络。
# DPoS
代理权益证明(DPoS)是一种经过修改的权益证明形式,网络参与者投票选举一系列代表(也称为证人)来验证和保护区块链。这些代表有 点类似于PoA中的权威节点,除非他们的权限可能被选民撤销。
在DPoS共识中,与PoS一样,投票权重与用户注入的投注金额成正比。这就产生了一个场景,即较多token持有者比较少token的持有者拥有 更多的投票权。从游戏理论的角度来看,这是有道理的,因为那些具有更多经济的“游戏中的皮肤”的人自然会有更大的动力来选出最有效的 代表证人。
此外,代表证人会收到验证每个区块的奖励,因此被激励保持诚实和有效 - 以免被替换。然而,有一些方法可以使“贿赂”变得相当合理; 例如,交易所可以提供存款利率(或者更加含糊地,使用交易所自己的资金建立一个很好的界面和功能),交易所运营商可以使用大量存款 进行DPoS共识投票。。
# 以太坊的共识
# Ethash简介
Ethash是以太坊工作量证明(PoW)算法,它依赖于数据集的初始纪元的生成,该数据集的大小约为1GB,称为有向无环图(DAG)。 DAG使用Dagger-Hashimoto算法的版本,它是Vitalik Buterin的Dagger算法和Thaddeus Dryja的Hashimoto算法 的组合。Dagger-Hashimoto算法是以太坊1.0使用的挖掘算法。随着时间的推移,DAG线性增长,每纪元(30,000块, 125小时)更新一次。
# 种子,缓存,数据生成
PoW算法涉及:
- 通过扫描DAG的先前块头来计算每个块的Seed。
- Cache 是一个16MB的伪随机缓存,根据种子计算,用于轻量级客户端中的存储。
- 来自cache的DAG Data Generation 在完整客户端和矿工上用于存储 (数据集中的每一项只依赖cache中的一小部分项目)
- 矿工通过随机抽取数据集的片段并将它们混合在一起进行挖掘。可以使用存储的缓存和低内存进行验证,以重新生成所需的数据集的 特定部分。
参考:
- Ethash-DAG: https://github.com/ethereum/wiki/wiki/Ethash-DAG (opens new window)
- Ethash Specification: https://github.com/ethereum/wiki/wiki/Ethash (opens new window)
- Mining Ethash DAG: https://github.com/ethereum/wiki/wiki/Mining#ethash-dag (opens new window)
- Dagger-Hashimoto Algorithm: https://github.com/ethereum/wiki/blob/master/Dagger-Hashimoto.md (opens new window)
- DAG Explanation and Images: https://ethereum.stackexchange.com/questions/1993/what-actually-is-a-dag (opens new window)
- Ethash in Ethereum Yellowpaper: https://ethereum.github.io/yellowpaper/paper.pdf#appendix.J (opens new window)
- Ethash C API Example Usage: https://github.com/ethereum/wiki/wiki/Ethash-C-API (opens new window)
# Polkadot简介
Polkadot是一种链间区块链协议,包括与权益证明(PoS)链的整合,允许Parachain在没有内部共识的情况下获得共识。
Polkadot包括:
- Relay-Chains 连接到所有Parachains并协调区块链之间的共识和交易传递,并使用验证函数通过验证PoV候选块的正确性 来促进Parachain交易的最终确定。
- Parachains(跨网络的并行链),它们是区块链,用于收集和并行处理交易以实现可伸缩性。
- 无需信任,交易直接在区块链之间转移,而不是通过中间人或分散交易所。
- 汇总安全,根据共识协议规则(Rules)检查Parachain交易有效性。通过结合由动态治理系统确定的每个集团成员的一定比 例的权益token资本来实现安全性。群组成员资格需要绑定来自Validators和Nominators的赌注token的输入,如果出现不良行为,可以 在试验中使用不当行为证明进行扣除。
- Bridges 通过解耦具有不同共识架构机制的区块链网络之间的链接来提供可扩展性。
- Collators 负责监管和维护特定的Parachain,方法是将其可用交易整理为有效性证明(PoV)候选块,向Validators报告以证明 交易有效并在块中正确执行。如果它有winning ticket(由最接近Golden Ticket的Polkadot地址的Collator签名)并且变得规范和最终 确定,则通过支付他们从创建PoV候选区块所收集的任何交易费来激励他们。Collators被给予Polkadot地址。胶合剂不与铆接标记粘合。
- Golden Ticket是包含奖励的每个Parachain的每个区块中的特定Polkadot地址。Collators被赋予一个Polkadot地址,并向 Validator提供由Collator签名的PoV候选块。奖励的获奖者在PoV候选区块中有一个Collator Polkadot地址,该区域最接近 Golden Ticket Polkadot地址
- Fisherman 监控Polkadot网络交易,以发现Polkadot社区的不良行为。将验证者带到法庭并证明他们表现得很糟糕的Fisherman 会被确认者的债券激励,因为债券被用作惩罚不良行为的惩罚。
- 验证者 是Parachain社区中的维护者,他们被部署到不同的Parachains来监管系统。验证者同意Merkle Trees的根源。验证者 必须使交易可用。渔民可以将验证员带到法庭,因为没有进行交易,相关的Collators可能会质疑该交易是否可以作为整理证明。
- 提名者(类似于PoW挖掘)被动监督并投票给他们认为可以通过赌注代币资助他们认可的确认者。
Polkadot的Relay-Chains使用Proof of Stake(PoS系统,其中结构化状态机(SM)并行执行多个拜占庭容错(BFT)共识,以便 SM过程收敛于越多个Parachain维度的包含有效候选者的解决方案跨的块。每个Parachain中的有效候选块是根据交易的可用性和有效性 确定的,因为根据共识机制,目标验证者(下一个块)只有在具有足够的交易信息时才能从源验证者(前一个块)执行传入消息。可用和 有效。验证人投票选择Collators使用规则达成共识的有效候选区块。
参考
- Polkadot link: https://polkadot.network (opens new window)
- Polkadot presentation at Berlin Parity Ethereum link: https://www.youtube.com/watch?v=gbXEcNTgNco (opens new window)
# 第十六章 Vyper:面向合约的编程语言
研究表明,具有跟踪漏洞的智能合约可能导致意外执行。https://arxiv.org/pdf/1802.06038.pdf (opens new window)[最近的一项研究] 分析了970,898份合约。它概述了跟踪漏洞的三个基本类别(已经导致以太坊用户的灾难性资金损失)。这些类别包括
- 自杀合约。可以被任意地址杀死的合约
- 贪婪的合约,在某个执行状态后无法释放ether
- 浪费合约,不经意地将ether释放到任意地址
Vyper是一种面向合约的实验性编程语言,面向以太坊虚拟机(EVM)。Vyper致力于通过简化代码并使其对人类可读而提供卓越的审计能力。 Vyper的一个原则是让开发人员几乎不可能编写误导性代码。这可以通过多种方式完成,我们将在下面介绍。
# 与 Solidity 比较
本节是那些正在考虑使用Vyper编程语言开发智能合约的人的参考。该部分主要对比了Vyper与Solidity的对比; 概览,合理的推理, 为什么Vyper不包括以下传统的面向对象编程(OOP)概念:
Modifiers:: 在Solidity中,你可以使用修饰器编写函数。例如,以下函数changeOwner
将在一个名为onlyBy
的修饰器中运行代码,
作为其执行的一部分。
function changeOwner(address _newOwner)
public
onlyBy(owner)
{
owner = _newOwner;
}
2
3
4
5
6
正如我们在下面看到的,名为onlyBy
的修饰器强制执行与所有权相关的规则。虽然修饰器很强大(能够改变函数体中发生的事情),但
它们也可能导致误导性的代码执行。例如,确保changeOwner
函数逻辑的唯一方法是每次实现代码时检查并测试onlyBy
修饰器。显然,
如果将来更改修改器,则调用它的函数可能会产生与最初预期不同的结果。
modifier onlyBy(address _account)
{
require(msg.sender == _account);
_;
}
2
3
4
5
总的来说,修饰器的通常用例是在函数执行之前执行单个检查。鉴于这种情况,Vyper的建议是完全取消修饰器,并简单地使用内联检查和 断言作为函数的一部分。这样做将提高审计能力和可读性,因为Vyper函数将在明显的视线中遵循逻辑内联序列,而不必引用已在别处编写 的修饰器代码。
类继承
继承允许程序员通过从现有软件库中获取预先存在的功能,属性和行为来利用预先编写的代码。继承功能强大,可以促进代码的
重用。Solidity支持多重继承以及多态,虽然这些被认为是面向对象编程的一些最重要的特性,但Vyper并不支持它们。Vyper坚持认为
继承的实现要求编码人员和审计人员在多个文件之间跳转,以便了解程序正在做什么。Vyper还了解优先级规则以及多个继承如何使代码
过于复杂而无法理解。鉴于Solidity https://github.com/ethereum/solidity/blob/release/docs/contracts#inheritance (opens new window)[关于继承的文档]
给出了多重继承为何有问题的例子,这是一个公平的陈述。
内联汇编
内联汇编为开发人员提供了以低级别访问以太坊虚拟机(EVM)的机会。使用内联汇编代码(在更高级别的源代码中)时,开发
人员可以通过直接访问EVM操作码指令来执行操作。例如,以下内联汇编代码通过使用EVM操作码mload在内存位置0x80处添加3。
3 0x80 mload add 0x80 mstore
如前所述,Vyper致力于为开发人员和代码审计人员提供最易读的代码。虽然内联汇编可以提供强大的细粒度控制,但Vyper编程语言 不支持它。
函数重载
具有相同名称和不同参数选项的多个函数定义会导致在任何给定时间调用哪个函数时会产生很多混淆。随着函数重载,编写
误导代码会更容易( foo(“hello”)记录“hello”但foo(“hello”,“world”)窃取你的资金。)函数重载的另一个问题是它使代码
更难以搜索,因为你必须跟踪哪个调用指的是哪个功能。
变量类型转换
类型转换是一种允许程序员将变量从一种数据类型转换为另一种数据类型的机制。
前置条件和后置条件
Vyper明确处理前置条件,后置条件和状态更改。虽然这会产生冗余代码,但它也允许最大的可读性和安全性。在Vyper中编写智能合约时,
开发人员应遵守以下3点。理想情况下,应仔细考虑3个点中的每个点,然后在代码中进行详细记录。这样做将改进代码的设计,最终使代码
更具可读性和可审计性。
- 条件 - 以太坊状态变量的当前状态/条件是什么
- 效果 - 这个智能合约代码对执行状态变量的条件有什么影响,即什么会影响,什么不会受到影响?这些影响是否与智能合约的意图一致?
- 交互 - 现在已经详尽地处理了前两个步骤,现在是运行代码的时候了。在部署之前,逻辑上逐步执行代码并考虑执行代码的所有可能的永久结果,后果和方案,包括与其他合约的交互。
# 一种新的编程范式
Vyper的创作为新的编程范式打开了大门。例如,Vyper正在删除类继承以及其他功能,因此可以说Vyper偏离了传统的面向对象编程(OOP) 范例,这很好。
历史上,OOP提供了一种表示现实世界对象的机制。例如,OOP允许实例化可以从person类继承的employee对象。然而,从价值转移和/或 智能合约的角度来看,那些渴望功能性编程范式的人会同意,交易性编程绝不适合上述传统的OOP范式。简而言之,交易计算是与现实世界 对象分开的世界。例如,你最后一次持有交易或正向链接业务规则的时间是什么时候?
似乎Vyper没有与OOP范例或函数式编程范例完全一致(完整的原因列表超出了本章的范围)。出于这个原因,在开发的早期阶段,我们能够 如此大胆地推出新的软件开发范例吗?一个致力于未来证明区块链可执行代码的人。一个可以防止在不可改变的环境中造成灾难性资金损失 的人。区块链革命中经历的过去事件有机地为这一领域的进一步研究和发展创造了新的机会。也许这种研究和开发的结果最终可能导致软件 开发的新的不变性范式分类。
# 装饰符
向 @private
@public
@constant
@payable
这样的装饰符在每个函数的开头声明。
Private
@private
使合约外部的函数无法访问此函数。
Public
@public
使该函数公开可见和可执行。例如,即使是以太坊钱包也会在查看合约时显示公共函数。
Constant
以 @constant
开始的函数不允许状态变量的改变,实际上,如果函数尝试更改状态变量,编译器将拒绝整个程序
(带有适当的警告)。如果该函数用于更改状态变量,则不要在函数的开头使用@ constant
。
Payable
只有以 @payable
开头声明的函数才能接收价值。
Vyper明确地实现了装饰符的逻辑。例如,如果一个函数前面有一个@appay
装饰符和一个@ constant
装饰符,那么Vyper代码编译
过程就会失败。当然,这是有道理的,因为常量函数(仅从全局状态读取的函数)永远不需要参与值的转移。此外,每个Vyper函数必须
以@ public
或@private
装饰符开头,以避免编译失败。同时使用@public
装饰符和@private
装饰符的Vyper函数也会导致编译
失败。
# 在线代码编辑器和编译器
Vyper在以下URL https://vyper.online (opens new window)上有自己的在线代码编辑器和编译器。这个Vyper在线编译器允许你仅使用Web浏览器编写 智能合约,然后将其编译为字节码,ABI和LLL。Vyper在线编译器具有各种预先编写的智能合约,以方便你使用。这些包括简单的公开拍卖, 安全的远程购买,ERC20 token等。
# 使用命令行编译
每个Vyper合约都保存在扩展名为.v.py的单个文件中。 安装完成后,Vyper可以通过运行以下命令来编译和提供字节码
vyper ~/hello_world.v.py
通过运行以下命令可以获得人类可读的ABI代码(JSON格式)
vyper -f json ~/hello_world.v.py
# 读写数据
智能合约可以将数据写入两个地方,即以太坊的全球状态查找树或以太坊的链数据。虽然存储,读取和修改数据的成本很高,但这些存储 操作是大多数智能合约的必要组成部分。
全局状态:: 给定智能合约中的状态变量存储在以太坊的全局状态查找树中,给定的智能合约只能存储,读取和修改与该合约地址相关的数据 (即智能合约无法读取或写入其他智能合约)。
Log:: 如前所述,智能合约也可以通过日志事件写入以太坊的链数据。虽然Vyper最初使用 pass:[]logpass:[] 语法来声明这些事件
,但已经进行了更新,使Vyper的事件声明更符合Solidity的原始语法。例如,Vyper声明的一个名为MyLog的事件最初是
MyLog: pass:[__]logpass:[__]({arg1: indexed(bytes[3])})
,Vyper的语法现在变为
MyLog: event({arg1: indexed(bytes[3])})
。需要注意的是,在Vyper中执行日志事件仍然是如下 log.MyLog("123")
。
虽然智能合约可以写入以太坊的链数据(通过日志事件),但智能合约无法读取他们创建的链上日志事件。尽管如此,通过日志事件写入 以太坊的链数据的一个好处是,可以在公共链上由轻客户端发现和读取日志。例如,挖到的块中的logsBloom值可以指示是否存在日志事件。 一旦建立,就可以通过日志路径获取 logs -> data inside a given transaction receipt。
# ERC20令牌接口实现
Vyper已将ERC20实施为预编译合约,并允许默认使用它。 Vyper中的合约必须声明为全局变量。声明ERC20变量的示例可以是
token: address(ERC20)
# 操作码(OPCODES)
智能合约的代码主要使用Solidity或Vyper等高级语言编写。编译器负责获取高级代码并创建它的低级解释,然后可以在以太坊虚拟机(EVM) 上执行。编译器可以提取代码的最低表示(在EVM执行之前)是操作码。在这种情况下,需要高级语言(如Vyper)的每个实现来提供适当的 编译机制(编译器)以允许(除其他之外)将高级代码编译到通用预定义的EVM操作码中。一个很好的例子是Vyper实现了以太坊的分片操作 码。
# 第十七章 DevP2P协议
# 节点间的通信 —— 一个简单的视角
以太坊节点之间通过简单的线路协议进行通信,形成一个虚拟或覆盖良好的网络 为实现这一目标,该协议称为ÐΞVp2p,使用RLP等技术和标准。
# 传输协议
为了提供机密性并防止网络中断,ÐΞVp2p节点使用RLPx消息,一种加密且经过身份验证的_transport协议。 RLPx使用类似于Kademlia的路由算法,Kademlia是用于分散的对等计算机网络的分布式哈希表(DHT)。 RLPx,作为底层传输协议,允许 “节点发现和网络形成” 。 RLPx的另一个显著特征是通过单个连接支持 多个协议 。
当ÐΞVp2p节点通过Internet进行通信时(通常情况下),它们使用TCP,它提供面向连接的介质,但实际上ÐΞVp2p节点 通过使用底层传输协议RLPx所提供的所谓设施(或消息),以数据包通信,允许它们通信发送和接收数据包。
数据包是 动态构建 dynamicically framed,前缀为_RLP_编码标头,经过加密和验证。通过帧头实现多路复用,帧头指定 分组的目的协议。
# 加密握手
通过握手建立连接,并且一旦建立,就将数据包加密并封装为帧。
此握手将分两个阶段进行,第一阶段涉及密钥交换,第二阶段将执行身份验证,作为DEVp2p的一部分,还将交换每个节点的功能。
# 安全 - 基本考虑因素
所有加密操作都基于secp256k1,并且每个节点都应该维护一个静态私钥,该私钥在会话之间保存和恢复。
在实施加密之前,数据包具有时间戳属性,以减少执行重放攻击的时间窗口。 建议接收方只接受最近3秒内创建的数据包。
数据包被签名。通过从签名中恢复公钥并检查它是否与预期值匹配来执行验证。
# ÐΞVp2p 消息和子协议
使用RLP,我们可以编码不同类型的数据,其类型由RLP的第一个条目中使用的整数值确定。 这样,ÐΞVp2p,基础线路协议 basic wire protocol,支持 任意的子协议 。
0x00-0x10
之间的 Message IDs 保留用于ÐΞVp2p消息。因此,假定 sub-protocols 的消息ID从“0x10”开始。
未在对等节点之间共享的子协议是 忽略的 。 如果共享相同(同名)子协议的多个版本,则数字最高的胜出。
# 基本建立通信 - 基本ÐΞVp2p消息
作为一个非常基本的例子,当两个对等节点发起他们的通信时,每个对等节点用另一个称为**“Hello” 的特殊 ÐΞVp2p**消息来迎接 另一个,该消息由“0x00”消息ID标识。 通过这个特定的 ÐΞVp2p “Hello” 消息,每个节点将向其对等的相关数据公开,从而允许通信以非常基本的级别开始。
在此步骤中,每个对等方将知道有关其对等方的以下信息。
- P2P协议的实现 版本。现在必须是'1`。
- 客户端软件标识,作为人类可读的字符串(例如
Ethereum(++)/ 1.0.0
)。 - 对等节点的capability name为长度为3的ASCII字符串。当前支持的能力名称为“eth”和“shh”。
- 对等节点的capability version为正整数。目前支持的版本是
eth
为34
,shh
为1
。 - 客户端正在侦听的端口。如果为“0”则表示客户端没有收听。
- 节点的唯一标识指定为512位散列。
# 断开连接 - 基本ÐΞVp2p消息
要执行有序的断开连接,要断开连接的节点将发送名为 “Disconnect” 的ÐΞVp2p消息,该消息由_“0x01”_消息ID标识。 此外,节点使用参数 “reason” 指定断开的原因。
“reason” 参数可以取值从“0x00”到“0x10”,例如“0x00”表示原因 “请求断开连接” 和“0x04”表示 “太多对等节点” 。
# 状态 - 以太坊子协议示例
该子协议由+0x00
消息-id标识。
此消息应在初始握手之后和任何与以太坊相关的消息之前发送,并通知其当前状态。
为此,节点向其对等方公开以下数据;
- Protocol version
- Network Id
- Total Difficulty of the best chain
- Hash of the best known block
- Hash of the Genesis block
# 已知的当前网络ID
这里是目前已知的网络ID列表:
- 0: Olympic; 以太坊公共预发布测试网
- 1: Frontier; Homestead,Metropolis,以太坊公共主网
- 1: Classic; (un)forked 公共以太坊Classic主网络,链ID 61
- 1: Expanse; 另一个以太坊实现,链ID 2
- 2: Morden; 公共以太坊测试网,现在是以太坊经典测试网
- 3: Ropsten; 公共跨客户端以太坊测试网
- 4: Rinkeby: 公共Geth以太坊测试网
- 42: Kovan; 公共Parity以太坊测试网
- 77: Sokol; 公共POA测试网
- 99: POA; 公共权威证明(PoA)以太网网络
- 7762959: Musicoin; 音乐区块链
# GetBlocks - 另一个子协议示例
该子协议由+ 0x05
message-id标识。
通过此消息,节点通过其自己的哈希向其对等方请求指定的块。
请求节点的方式是通过包含它们所有哈希值的列表,将消息采用以下形式;
[+0x05: P, hash_0: B_32, hash_1: B_32, ...]
请求节点必须没有包含所有请求的块的响应消息,在这种情况下,它必须再次请求那些尚未由其对等方发送的消息。
# 节点标识和声誉
ÐΞVp2p节点的标识是secp256k1公钥。
客户端可以自由标记新节点并使用节点ID作为_决定节点的信誉_的方法。
他们可以存储给定ID的评级并相应地给予优先权。
# 第十八章 以太坊标准
# 以太坊改进提案(EIPs)
https://eips.ethereum.org/ (opens new window)
来自EIP-1:
EIP代表以太坊改进提案。EIP是一个设计文档,为以太坊社区提供信息,或描述以太坊或其过程或环境的新功能。EIP应提供该功能的简明技术规范和该功能的基本原理。EIP作者负责在社区内建立共识并记录不同意见。

Figure 1. Ethereum改进提案工作流程
# 以太坊征求意见(ERCs)
Request for Comments(RFC)是一种用于为互联网引入技术和组织指南的方法,因为它们是由https://www.ietf.org (opens new window)[Internet Engineering Task Force] 提出的。ERCS包括为以太坊网络设置标准的类似指南。以下部分提供了由以太坊开发人员社区开发和接受的最新列表。
ERCs的增加是通过https://github.com/ethereum/EIPs (opens new window)[EIPs],以太坊改进协议来完成的, 这是对比特币自己的BIP的致敬。EIP由开发人员编写并提交给同行评审,评估其有用性,并且能够增加现有ERC的实用性。如果他们被接受, 他们最终将成为ERC标准的一部分。
# 最重要的EIP和ERC表
Table 1. Important EIPs and ERCs
EIP/ERC # | Title/Description | Author | Layer | Status | Created |
EIP-1 | EIP Purpose and Guidelines | Martin Becze, Hudson Jameson | Meta | Final | |
EIP-2 | Homestead Hard-fork Changes | Vitalik Buterin | Core | Final | |
EIP-5 | Gas Usage for RETURN and CALL* | Christian Reitwiessner | Core | Draft | |
EIP-6 | Renaming SUICIDE Opcode | Hudson Jameson | Interface | Final | |
EIP-7 | DELEGATECALL | Vitalik Buterin | Core | Final | |
EIP-8 | devp2p Forward Compatibility Requirements for Homestead | Felix Lange | Networking | Final | |
EIP-20 | ERC-20 Token Standard. Describes standard functions a token contract may implement to allow DApps and wallets to handle tokens across multiple interfaces/DApps. Methods include: totalSupply, balanceOf(address), transfer, transferFrom, approve, allowance. Events include: Transfer (triggered when tokens are transferred), Approval (triggered when approve is called). | Fabian Vogelsteller, Vitalik Buterin | ERC | Final | Frontier |
EIP-55 | Mixed-case checksum address encoding | Vitalik Buterin | ERC | Final | |
EIP-86 | Abstraction of transaction origin and signature. Sets the stage for "abstracting out" account security and allowing users to create "account contracts," moving toward a model where in the long term all accounts are contracts that can pay for gas, and users are free to define their own security models that perform any desired signature verification and nonce checks (instead of using the in-protocol mechanism where ECDSA and the default nonce scheme are the only "standard" way to secure an account, which is currently hardcoded into transaction processing). | Vitalik Buterin | Core | Deferred (to be replaced) | Constantinople |
EIP-96 | Blockhash and state root changes. Stores blockhashes in the state to reduce protocol complexity and need for complex client implementations to process the BLOCKHASH opcode. Extends range of how far back blockhash checking may go, with the side effect of creating direct links between blocks with very distant block numbers to facilitate much more efficient initial light client syncing. | Vitalik Buterin | Core | Deferred | Constantinople |
EIP-100 | Change difficulty adjustment to target mean block time and including uncles. | Vitalik Buterin | Core | Final | Metropolis Byzantinium |
EIP-101 | Serenity Currency and Crypto Abstraction. Abstracts ether up a level with the benefit of allowing ether and subtokens to be treated similarly by contracts, reduces the level of indirection required for custom-policy accounts such as multisigs, and purifies the underlying Ethereum protocol by reducing the minimal consensus implementation complexity. | Vitalik Buterin | Active | Serenity feature | Serenity Casper |
EIP-105 | Binary sharding plus contract calling semantics. "Sharding scaffolding" EIP to allow Ethereum transactions to be parallelized using a binary tree sharding mechanism, and to set the stage for a later sharding scheme. Research in progress; see https://github.com/ethereum/sharding. | Vitalik Buterin | Active | Serenity feature | Serenity Casper |
EIP-137 | Ethereum Domain Name Service - Specification | Nick Johnson | ERC | Final | |
EIP-140 | New Opcode: REVERT. Adds REVERTopcode instruction, which stops execution and rolls back the EVM execution state changes without consuming all provided gas (instead the contract only has to pay for memory) or losing logs, and returns to the caller a pointer to the memory location with the error code or message. | Alex Beregszaszi, Nikolai Mushegian | Core | Final | Metropolis Byzantinium |
EIP-141 | Designated invalid EVM instruction | Alex Beregszaszi | Core | Final | |
EIP-145 | Bitwise shifting instructions in EVM | Alex Beregszaszi, Paweł Bylica | Core | Deferred | |
EIP-150 | Gas cost changes for IO-heavy operations | Vitalik Buterin | Core | Final | |
EIP-155 | Simple replay attack protection. Replay Attack allows any transaction using a pre-EIP-155 Ethereum node or client to become signed so it is valid and executed on both the Ethereum and Ethereum Classic chains. | Vitalik Buterin | Core | Final | Homestead |
EIP-158 | State clearing | Vitalik Buterin | Core | Superseded | |
EIP-160 | EXP cost increase | Vitalik Buterin | Core | Final | |
EIP-161 | State trie clearing (invariant-preserving alternative) | Gavin Wood | Core | Final | |
EIP-162 | Initial ENS Hash Registrar | Maurelian, Nick Johnson, Alex Van de Sande | ERC | Final | |
EIP-165 | ERC-165 Standard Interface Detection | Christian Reitwiessner et al. | Interface | Draft | |
EIP-170 | Contract code size limit | Vitalik Buterin | Core | Final | |
EIP-181 | ENS support for reverse resolution of Ethereum addresses | Nick Johnson | ERC | Final | |
EIP-190 | Ethereum Smart Contract Packaging Standard | Piper Merriam et al. | ERC | Final | |
EIP-196 | Precompiled contracts for addition and scalar multiplication on the elliptic curve alt_bn128. Required in order to perform zkSNARK verification within the block gas limit. | Christian Reitwiessner | Core | Final | Metropolis Byzantinium |
EIP-197 | Precompiled contracts for optimal ate pairing check on the elliptic curve alt_bn128. Combined with EIP-196. | Vitalik Buterin, Christian Reitwiessner | Core | Final | Metropolis Byzantinium |
EIP-198 | Big integer modular exponentiation. Precompile enabling RSA signature verification and other cryptographic applications. | Vitalik Buterin | Core | Final | Metropolis Byzantinium |
EIP-211 | New opcodes: RETURNDATASIZE and RETURNDATACOPY. Adds support for returning variable-length values inside the EVM with simple gas charging and minimal change to calling opcodes using new opcodes RETURNDATASIZEand RETURNDATACOPY. Handles similar to existing calldata, whereby after a call, return data is kept inside a virtual buffer from which the caller can copy it (or parts thereof) into memory, and upon the next call, the buffer is overwritten. | Christian Reitwiessner | Core | Final | Metropolis Byzantinium |
EIP-214 | New opcode: STATICCALL. Permits non-state-changing calls to itself or other contracts while disallowing any modifications to state during the call (and its subcalls, if present) to increase smart contract security and assure developers that re-entrancy bugs cannot arise from the call. Calls the child with STATIC flag set to truefor execution of child, causing exception to be thrown upon any attempts to make state-changing operations inside an execution instance where STATIC is true, and resets flag once call returns. | Vitalik Buterin, Christian Reitwiessner | Core | Final | Metropolis Byzantinium |
EIP-225 | Rinkeby testnet using proof of authority where blocks are only mined by trusted signers. | Péter Szilágyi | Homestead | ||
EIP-234 | Add blockHash to JSON-RPC filter options | Micah Zoltu | Interface | Draft | |
EIP-615 | Subroutines and Static Jumps for the EVM | Greg Colvin, Paweł Bylica, Christian Reitwiessner | Core | Draft | |
EIP-616 | SIMD Operations for the EVM | Greg Colvin | Core | Draft | |
EIP-681 | URL Format for Transaction Requests | Daniel A. Nagy | Interface | Draft | |
EIP-649 | Metropolis Difficulty Bomb Delay and Block Reward Reduction. Delayed the Ice Age (aka Difficulty Bomb) by 1 year, and reduced the block reward from 5 to 3 ether. | Afri Schoedon, Vitalik Buterin | Core | Final | Metropolis Byzantinium |
EIP-658 | Embedding transaction status code in receipts. Fetches and embeds a status field indicative of success or failure state to transaction receipts for callers, as it’s no longer possible to assume the transaction failed if and only if it consumed all gas after the introduction of the REVERT opcode in EIP-140. | Nick Johnson | Core | Final | Metropolis Byzantinium |
EIP-706 | DEVp2p snappy compression | Péter Szilágyi | Networking | Final | |
EIP-721 | ERC-721 Non-Fungible Token Standard. A standard API that allows smart contracts to operate as unique tradable non-fungible tokens (NFTs) that may be tracked in standardized wallets and traded on exchanges as assets of value, similar to ERC20. CryptoKitties was the first popularly adopted implementation of a digital NFT in the Ethereum ecosystem. | William Entriken, Dieter Shirley, Jacob Evans, Nastassia Sachs | Standard | Draft | |
EIP-758 | Subscriptions and filters for completed transactions | Jack Peterson | Interface | Draft | |
EIP-801 | ERC-801 Canary Standard | ligi | Interface | Draft | |
EIP-827 | ERC827 Token Standard. An extension of the standard interface ERC20 for tokens with methods that allow the execution of calls inside transfer and approvals. This standard provides basic functionality to transfer tokens, as well as allowing tokens to be approved so they can be spent by another on-chain third party. Also, it allows the developer to execute calls on transfers and approvals. | Augusto Lemble | ERC | Draft | |
EIP-930 | ERC930 Eternal Storage. The ES (Eternal Storage) contract is owned by an address that has write permissions. The storage is public, which means everyone has read permissions. It stores the data in mappings, using one mapping per type of variable. The use of this contract allows the developer to migrate the storage easily to another contract if needed. | Augusto Lemble | ERC | Draft |
# 第十九章 以太坊分叉历史
大多数硬分叉计划作为路线图的一部分,并包含社区普遍认同的更新; 这通常被称为共识。然而,一些硬分叉并不总是保持共识,这导致 多个不同的区块链。导致以太坊/以太坊经典分裂的事件就是这种情况。
# 以太坊经典(ETC)
在以太坊社区的成员继续使用时间敏感的硬分叉(“DAO Hard Fork”)之后,以太坊经典就出现了。2016年7月20日,在以192万的区块 高度上,以太坊通过硬分叉引入了不规则的状态变化,从而退还大约360万的ether,这些以太币来自一个名为The DAO的智能合约。
社区中的一些人不同意这种变化,认为这违反了以太坊的不变性; 他们选择在以太坊经典的绰号下继续保持原有的链条。虽然分裂本身 最初是意识形态的,但两个链条已经发展成为它们各自独立的实体。
# 去中心化的自治组织(The DAO)
DAO是由Slock.It创建的; 一支技术精湛的开发团队,包括以前的以太坊创始成员。Slock.It 将DAO视为基于社区为项目提供的资金的 一种方式。其核心思想是提交提案,管理者人将管理提案,资金将从以太坊社区的投资者筹集,如果项目证明成功,那么投资者将获得一 部分利润。
DAO也是以太坊 token中的第一个实验之一。参与者不是直接用Ether资助项目,而是将他们的以太币交换为DAO代币,使用它们对项目资 金进行投票,然后能够将它们交换为以太币。
DAO token能够在2016年4月5日至4月30日期间的众筹中购买,占据了当时总价值约1.5亿美元的全部以太存款的近14%[1]。
# 重入( Re-Entrancy )Bug
6月9日,开发商Peter Vessenes和Chriseth报告称,大多数基于以太坊的合约管理基金都可能容易受到可以清空合约资金的漏洞[2]的 影响。几天后(6月12日)斯蒂芬·塔尔(Slock.It的创始人)报告说,DAO的代码并不容易受到彼得和克里斯描述的错误的影响。 [3] 令人担忧的DAO贡献者暂时松了一口气,直到5天后(6月17日),一名未知的攻击者(“DAO攻击者”)开始使用类似于6月9日描述的 漏洞利用DAO [4] 。最终,DAO攻击者从DAO中吸取了大约360万个ether。
同时,一群自称为Robinhood Group(RHG)的志愿者开始使用相同的漏洞来提取剩余的资金。6月21日,RHG宣布[5]他们已经获得 了另外70%的DAO,约720万以太,并计划将其退还社区。RHG的快速行动和思考被给予了很多感谢和赞扬,这有助于保护社区的大部分ether。
# Re-Entrancy技术
虽然Phil Daian [6] 描述了对该错误的更详细和详尽的解释,但简短的解释是可以在以太坊虚拟机上同时多次调用合约函数)。 这允许DAO攻击者反复请求提取ether,并且在合约记录DAO攻击者已经提取之前,攻击者再次提取。
# Re-Entrancy 攻击流程
想象一下,你的银行账户中有100美元,你可以向你的银行出纳员提取任意数量的提款单。银行出纳员按顺序为你提供每张单据的金额, 并且只有在所有单据结束时才会记录你的提款。如果你给他们带来三张单,每张100美元怎么办?如果你给他们带来三千个怎么办?
换句话说,流程是:
- DAO攻击者要求DAO合约提取DAO tokens。
- 在合约更新其DAO被提取的记录之前,DAO攻击者要求合约再次提取DAO。
- 尽可能重复第二步。
- 合约最终记录了一次DAO的提取,失去了在此期间发生的取款。
# DAO硬分叉
DAO中的一想安全措施是所有提款请求都要延迟28天。这为社区提供了一个简短的时间来讨论如何处理漏洞利用。从大约6月17日到7月20日, DAO攻击者将无法将他们的DAO token转换为ether。
一些开发人员专注于寻找可行的解决方案,并在这么短的时间内探索了多种途径。其中包括6月24日宣布的DAO软分叉推迟DAO的退出,直到 达成共识[7],以及7月15日宣布的DAO硬分叉,以不正常的方式扭转DAO攻击影响的状态改变[8]。
6月28日,开发人员在DAO软分叉[9]中发现了一个DoS漏洞,并得出结论,DAO硬分叉将是fork之路上的唯一可行选择。DAO硬分叉将把 所有投资于DAO的ether转移到新的退款智能合约中,允许ether的原始所有者要求全额退款。这为返还被黑的资金提供了解决方案,但 也意味着干扰网络上特定地址的余额;但他们是孤立的。在DAO的部分中也会有一些剩余的ether,称为childDAO [12]。一组受托 人将手动授权剩余的ether;当时的价值约为6-7百万美元[8]。
随着时间的推移,多个以太坊开发团队创建了允许用户决定是否要启用此分叉的客户端。但是,客户端创建者想要决定是否选择 opt-in (默认不分叉),或选择opt-out(默认分叉)。7月15日,Carbonvote.com上的投票开始了[10]。7月16日,在块[1,894,000] [11],它被关闭。在以太供应总票数的5.5%中,约80%的选票(约占总供应量的4.5%)投票选择opt-out。选择opt-out的投票的四分 之一来自单一地址[12]。
最终决定成为选择opt-out,反对DAO硬分叉的人需要通过更改他们运行的软件中的配置选项来不分叉。
7月20日,在块1,920,000 [13] 以太坊实施了DAO硬分叉 [14],因此创建了两个以太坊,一个支持不规则的状态变化,另一个与它相对。
当硬分叉的以太坊(现今的以太坊)获得了大部分采矿权时,许多人认为达成共识并且少数群体链将逐渐消失; 和以前的分叉一样。尽管 如此,以太坊社区的相当大一部分开始支持原来的区块链,后来被称为以太坊经典。
几天之内,几个交易所开始列出以太坊(“ETH”)和以太坊经典(“ETC”)。由于硬分叉的性质,所有在分拆时持有ether的以太网用户现在 都在两个区块链中都持有资金,在Poloniex于7月24日列出ETC后,ETC的市场价值很快就建立了 [15]。
# 硬分叉的讨论
在DAO硬分叉前几周,/r/ethereum subreddit上发生了很多讨论。一些流行/关键的论点总结如下。
论点 | 原因 | 反对 |
责任/正义 | 如果可能的话,社区可以负责确定是否发生了盗窃并且应该纠正。有道德要求。 | 确定盗窃是否已经发生并且应该纠正的责任应该只由法律机构来完成。如果受影响的各方参与决策,则无法消除偏见。 |
DAO协议 | DAO的大多数参与者无法正确评估代码,因此他们不能同意受DAO代码的约束。 | DAO的条款和条件[23]的开头段落声明“…… DAO的代码控制并阐述了DAO创作的所有条款。” |
区块链不变性 | 区块链不变性是一种社会结构,因此如果多数人同意,我们可以改变它。 | 区块链不变性是一种社会结构,因此强制执行不变性非常重要。 |
选择加入与选择退出 | 社区可以选择Hard Fork是选择加入还是选择退出。我们投票决定是选择退出。 | 历史上Hard Forks是选择加入(即比特币)而非投票是不投票。在约1天的时间内,选择退出投票仅占总供应投票的4.5%。 [12] |
# DAO硬分叉的时间线
- 4月5日:Slock.It 在Dejavu Security[16]的安全审计之后创建了DAO
- 4月30日:DAO众筹推出[17]
- 5月27日:DAO众筹结束
- 6月9日:发现了潜在的递归调用错误,并认为它会影响跟踪用户余额的许多Solidity合约[2]
- 6月12日:Stephen Tual宣布DAO资金没有风险[3]
- 6月17日:DAO被利用,发现的bug的一个变种(称为“重新进入的bug”)被用来开始耗尽资金; 最终攫取了约30%的资金。[6]
- 6月21日:RHG宣布它已经确保了存储在DAO中的其他~70%的以太网。[5]
- 6月24日:通过Geth和Parity客户通过选择加入信号宣布软叉投票。这旨在暂时扣留资金,直到社区可以更好地决定做什么。[7]
- 6月28日:软叉中发现了一个漏洞,它已被废弃。[9]
- 6月28日至7月15日:用户辩论是否硬分叉。大多数争论发生在/r/ethereum subreddit上。
- 7月15日:DAO Hard Fork被提议撤销DAO攻击。[8]
- 7月15日:对carbonvote进行投票以决定DAO Hard Fork是否选择加入(默认情况下不分叉)或选择退出(默认为fork)。[10]
- 7月16日:以太供应总票数的5.5%,约80%的选票(约占总供应量的4.5%)是选择退出硬分叉。支持投票的四分之一来自一个地址。[11] [12]
- 7月20日:硬分叉发生在1,920,000块。[13] [14]
- 7月20日:反对DAO Hard Fork的人继续运行旧的非硬分叉客户端软件。这会导致在两个链上重放交易的问题。[18]
- 7月24日:Poloniex在股票代码ETC下列出原始的以太坊链; 这是第一次交换。[15]
- 8月10日:RHG将290万回收的ETC转移至Poloniex,以便在Bity SA的建议下将其转换为ETH。RHG总持有量的14%从ETC转换为ETH和其他加密货币。Poloniex冻结了另外86%的沉积ETH。[19]
- 8月30日:冻结的资金由Poloniex发送回RHG。然后RHG在ETC链上设立退款合约。[20] [21]
- 12月11日:IOHK的ETC开发团队组建。由以太坊创始成员Charles Hoskinson领导。
- 2017年1月13日:更新ETC网络以解决交易重播问题。这两个链现在在功能上是分开的。[22]
- 2月20日:ETCDEVTeam表格。早期ETC开发人员Igor Artamonov(splix)领导。
# 以太坊和以太坊经典
虽然最初的分裂以DAO为中心,但以太坊和以太坊经典现在是独立的项目。完整的差异是不断发展的,而且过于广泛而无法在本章涵盖, 值得注意的是,这些链条在核心发展和社区结构方面确实存在显着差异。
# 技术差异
# EVM
对于大多数部分(截至2018年4月),两个网络保持高度兼容。为一条链生成的合约代码在另一条链上按预期运行。尽管EVM操作系统的 差异很小(参见EIPs: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-140.md (opens new window)[140], link:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-145.md (opens new window)[145], 和link:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-214.md (opens new window)[214])
# 核心网络开发
所有区块链最终都有很多用户和贡献者。但是,由于开发此类软件所需的专业知识,核心网络开发(运行网络的代码)通常由分散的小组 完成。因此,这些小组生成的代码与实际运行网络的代码密切相关。
Ethereum | Ethereum Classic |
以太坊基金会和志愿者。 | ETCDEV, IOHK, 和志愿者 |
# 意识形态差异
以太坊和以太坊经典之间最大的物质差异之一是意识形态,它以两种主要方式表现出来:不变性和社区结构。
# 不变性
在区块链的背景下,不变性指的是区块链历史的保存。
Ethereum | Ethereum Classic |
遵循一种俗称“治理”的哲学。这种理念允许参与者以不同程度的代表性投票,在某些情况下(例如DAO攻击)改变区块链。 | 遵循一种理念,即一旦数据出现在区块链上,就不能被其他人修改。这是与比特币,Litecoin和其他加密货币共享的理念。 |
[[eth_etc_differences_community_structure]]
# 社区结构
虽然区块链旨在分散,但它们周围的世界大部分都是集中的。以太坊和以太坊经典以不同的方式处理这一现实。
Ethereum | Ethereum Classic |
以太坊基金会所有:/r/ethereum Subreddit, ethereum.org 网站, 论坛, GitHub (ethereum), Twitter (@ethereum), Facebook, 和 Google+ account. | 由单独的实体所有:/r/ethereumclassic Subreddit, the ethereumclassic.org 网站, 论坛, GitHubs (ethereumproject, ethereumclassic, etcdevteam, iohk, ethereumcommonwealth), Twitter (@eth_classic), Telegrams, 和 Discord. |
# 着名的以太坊分叉的时间表
在以太坊也发生了其他几个分叉。其中一些是硬分叉,因为它们直接从预先存在的以太坊网络中分离出来。其他是软分叉:它们使用 以太坊的客户端/节点软件,但运行完全独立的网络,没有与以太坊共享的任何历史记录。在以太坊的生活中可能会有更多的分叉。
还有一些其他项目声称是以太坊分叉,但实际上是基于ERC20 token并在以太坊网络上运行。其中两个例子是EtherBTC(ETHB)和 以太坊修改(EMOD)。这些不是传统意义上的分叉,有时也可称为空投。
- Expanse是以太坊区块链的第一个获得牵引力的分支。它是在2015年9月7日通过比特币谈话论坛宣布的。实际的分叉发生在一周后 的2015年9月14日,块高度为800,000。它最初由Christopher Franko和James Clayton创立。他们的愿景是创建一个先进的链: “身份,治理,慈善,商业和公平”。
- EthereumFog(ETF)于2017年12月14日推出,分块高度为4730660。他们的目标是通过专注于雾计算和分散存储来开发“世界分散 雾计算”。关于这实际上会带来什么的信息仍然很少。
- EtherZero(ETZ)于2018年1月19日发布,块高4936270,块高4936270。其值得注意的创新是引入了masternode架构并取消了 智能合约的交易费用,以实现更广泛的DAPP。以太网社区的一些著名成员MyEtherWallet和MetaMask遭到了一些批评,原因是围绕 开发缺乏明确性以及对可能的网络钓鱼的一些指责。
- EtherInc(ETI)于2018年2月13日发布,高度为5078585,重点是建立分散的组织。他们还宣布减少封锁时间,增加矿工奖励, 取消叔叔奖励并设置可开采硬币的上限。它们使用与以太坊相同的私钥,并实施了重放保护,以保护原始非重制链上的ether。
# 参考
[2] http://vessenes.com/more-ethereum-attacks-race-to-empty-is-the-real-deal/ (opens new window)
[4] http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit (opens new window)
[5] https://www.reddit.com/r/ethereum/comments/4p7mhc/update_on_the_white_hat_attack/ (opens new window)
[6] http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/ (opens new window)
[7] https://blog.ethereum.org/2016/06/24/dao-wars-youre-voice-soft-fork-dilemma/ (opens new window)
[8] https://blog.slock.it/hard-fork-specification-24b889e70703 (opens new window)
[10] https://blog.ethereum.org/2016/07/15/to-fork-or-not-to-fork/ (opens new window)
[12] https://elaineou.com/2016/07/18/stick-a-fork-in-ethereum/ (opens new window)
[14] https://blog.ethereum.org/2016/07/20/hard-fork-completed/ (opens new window)
[15] https://twitter.com/poloniex/status/757068619234803712 (opens new window)
[16] https://blog.slock.it/deja-vu-dao-smart-contracts-audit-results-d26bc088e32e (opens new window)
[17] https://blog.slock.it/the-dao-creation-is-now-live-2270fd23affc (opens new window)
[20] https://medium.com/@jackfru1t/the-robin-hood-group-and-etc-bdc6a0c111c3 (opens new window)
[23] https://web.archive.org/web/20160429141714/https://daohub.org/explainer.html/ (opens new window)
[24] https://ethereumclassic.github.io/blog/2016-12-12-TeamGrothendieck/ (opens new window)
全书完结
# 打赏

