以太坊深度研究合集(2017-2018 旧文整合)
本文整合了 2017 年 10 月到 2018 年 8 月间一系列以太坊学习与实战笔记,包括工作机制、Gas 系统、事务与消息调用、随机数问题、硬分叉、谢灵点应用、为什么以太坊不适合做联盟链,以及 CentOS 6.7 上 geth 私链搭建、web3 安装、solidity 智能合约部署等实战。三篇 2018-08-29 的"旧文一篇"已统一收录于第五至第七章,作者当时的"旧文回顾"语气一并保留。
一、概述
以太坊(Ethereum)是一台 transactional singleton machine with shared-state——事务性状态共享的单例机器。实际上就是逻辑上唯一,但物理上由多个节点维护的共识中的 world computer。这台机器的状态是由事务变迁驱动的:

以太坊的核心区别于比特币的设计点:
- 引入 EVM 与图灵完备的智能合约
- 账户模型而非 UTXO
- 用 Gas 系统作为算力定价与防滥用机制
- 默克尔帕特里夏树存储状态、事务和收据
- “幽灵协议”(GHOST = Greedy Heaviest Observed Subtree):只在拥有最大计算量的路径上进行计算(这个协议是从比特币那里来的吗?)
二、《以太坊到底是如何工作》读书笔记
参考原文:How does Ethereum work, anyway?
2.1 账户与事务
外部账户由私钥控制,内部账户由代码控制。

外部账户可以主动发起事务,内部账户只有收到事务以后才能发起内部事务。

2.2 账户的状态构成
一个账户的状态总是由四个组件构成:
- nonce:如果这是个外部账户,则这个数字代表了这个账户地址发出的事务数。如果这是个合约账户,则这个数字代表了这个账户创造的合约数量。这两种情况下,nonce 都不是随机数
- balance:这个地址拥有的 Wei 数量。一个以太币有个 1e+18 Wei
- storageRoot:默认为空。Merkle Patricia 树的根
- codeHash:对于内部账户,就是 EVM 代码的散列值(意味着代码存在别处)。对于外部账户,这是空字符串的散列值
2.3 以太坊里的默克尔树
以太坊的默克尔帕特里夏树的叶子节点是把地址映射到账户(状态)。

可以看出来状态树的叶子节点就是状态,特别地,状态里还有另一颗默克尔树的根。
同样的多叉 trie 树还用来存储事务和收据(receipts),一共有三种 trie:状态 trie、事务 trie 和收据 trie。
2.4 完整节点与轻节点
完整节点要下载整条链(从创世区块到当前头区块),执行里面包含的所有事务,否则无以挖矿。
轻节点如果不用执行每条事务或者查询历史信息,只要下载头链即可。
轻节点使用"Merkle Proof"的证明方式来验证一片数据:

步骤如下:
- 要验证的数据 chunk 和它的散列值
- 默克尔树的根
- branch(从数据 chunk 到根的所有伙伴散列值,所以这幅图里所有深绿色的节点,都要加入验证)
问题是,这样需要单独下载 branch 到底提升了多少性能呢?这种部分知识证明到底解决了什么查找问题?
2.5 Gas 与支付
在每个事务里,发送者设置 gas limit 和 gas price,实际花掉的以太币是这两个值的乘积。这两个值和区块/矿工眼里的 gas limit 和 gas price 还不太一样。
假设一个矿工设置了如下的两个参数值:

最终扣减账户内以太币的过程则是:

以太币不够,那么状态会回滚,而且在系统中多了一条事务执行失败记录,被用掉的以太币不会被返还:

事务执行花掉的钱最终要付给"beneficiary"地址,实际上就是矿工地址。
操作要付费,存储也要按照每三十二字节对齐一一付费。
为什么要付费呢?因为这个网络操作很贵,要支付矿工的基本维护费用(同中本聪分析的理性逐利一致),而且付费机制可以防止恶意程序拖垮全网。
2.6 事务的内容
以太坊是事务性状态机,逻辑上只有一个。
事务是由外部账户生成的密码学签名的指令片,经序列化后提交给区块链。
有两种事务:消息调用和合约创建。
所有事务都包含以下内容:
- nonce:这个发送方地址名下创建的事务数
- gasPrice:sender 愿意付的 gas 价格
- gasLimit:发送方愿意支付的 gas 数量上限
- to:接收方地址
- value:要发给接收方的价值。对于合约事务,传送的价值将被存储为合约的初始余额
- v,r,s:用来生成标识这个发送者的签名
- init:生成合约的程序。只被运行一次,它的返回值就是合约本身。猜测应该就是 ABI 和二进制代码
- data:只为了消息调用而存在的参数值。也就是调用的输入数据
内部事务和外部事物可以说是如出一辙,是不由外部账户生成,也不会被序列化,只存在于以太坊执行环境里的虚拟对象。内部事务也不含 gasLimit,这就要求最初的外部事务的 gasLimit 必须能够覆盖掉所有的衍生 sub-execution。sub-executions 出现不够 gas 的情况,会 revert 掉它的子 sub-execution,而不会 revert 掉 parent-execution(为什么?)。
2.7 Ommers 叔叔区块
以太坊的出块时间大约 15 s。
叔叔区块的奖励是为了奖励那些产生叔叔区块的矿工。
2.8 区块头信息
区块头包含以下信息:
- parentHash:父区块的头的散列——所以区块散列实际上是区块头的散列
- ommersHash:当前区块的 ommers 列表的散列
- beneficiary:生成这个区块的受益人的收款地址
- stateRoot:state trie 的根
- transactionRoot:transaction trie 的根
- receiptsRoot:receipt trie 的根
- logsBloom:用布隆过滤器来有效存 log 的地方(数据大头)
- difficulty:当前区块的难度级别
- number:当前区块的序号。创世区块是 0,每个区块加一
- gasLimit:当前区块的 gasLimit
- gasUsed:这个区块使用掉的 gas 总数
- timestamp:这个区块开始奠基(inception)的时间戳
- extraData:区块附言
- mixHash:与 nonce 结合说明计算量的散列值
- nonce:与 mixHash 结合说明计算量的散列值(为什么不是随机值?)

2.9 事务收据
区块头里存储的日志信息包含事务收据。每个 transaction 都有一个收据。 收据包括:
- 块号(区块 id1?)
- 块散列值(区块 id2?)
- 事务散列值(事务 id?)
- 当前事务消耗掉的 gas
- 执行完这个事务后,这个区块累计使用的 gas 值
- 执行事务生成的 log
- 其他(是什么?)
2.10 区块难度

Hd 就是难度。所以在这里 n 并不是数学游戏的输入,而是 nonce 的目标?
2.11 事务执行
计算初始 gas 的过程见原文。
执行过程的每一步,都会生成 log,也会生成一个逐渐减少的 refund balance。
事务执行后,以太坊得到了唯一的确定性状态。
2.12 合约创建
先创建一个 nonce 为空,codeHash 为空的 account,然后执行 init,把生成的合约代码和账户关联起来。
2.13 消息调用
消息调用类似合约创建。
2.14 执行模型
很复杂,还是看原文。
2.15 PoW 的原理

还是 ETHASH 算法,还是很复杂。但重点是 m 和 n 分别是 mixhash 和 nonce,这俩联合起来才能 match 这个算法。
三、核心概念
3.1 Gas 系统
根据以太坊开发教程官方文档:
One important aspect of the way the EVM works is that every single operation that is executed inside the EVM is actually simultaneously executed by every full node. This is a necessary component of the Ethereum 1.0 consensus model, and has the benefit that any contract on the EVM can call any other contract at almost zero cost, but also has the drawback that computational steps on the EVM are very expensive. Roughly, a good heuristic to use is that you will not be able to do anything on the EVM that you cannot do on a smartphone from 1999. Acceptable uses of the EVM include running business logic (“if this then that”) and verifying signatures and other cryptographic objects; at the upper limit of this are applications that verify parts of other blockchains (eg. a decentralized ether-to-bitcoin exchange); unacceptable uses include using the EVM as a file storage, email or text messaging system, anything to do with graphical interfaces, and applications best suited for cloud computing like genetic algorithms, graph analysis or machine learning.
In order to prevent deliberate attacks and abuse, the Ethereum protocol charges a fee per computational step. The fee is market-based, though mandatory in practice; a floating limit on the number of operations that can be contained in a block forces even miners who can afford to include transactions at close to no cost to charge a fee commensurate with the cost of the transaction to the entire network; see the whitepaper section on fees for more details on the economic underpinnings of our fee and block operation limit system.
3.2 事务、消息与消息调用
综合 StackExchange What is the difference between a “call”, “message call” and a “message” 下的回复,得出此节。
Call 是一个在不同的上下文下含义很混乱的词汇。
Message 是带有数据载荷或价值,在合约到合约之间传递的东西(合约可能有独立账户,也可能没有!)。Message 到达目标账户后,如果目标账户含有代码,则目标账户会产生状态迁移,这时候 Message 就产生了 Message Call。Message 不会因为挖矿延迟,他们本身就是 transaction 执行的一部分。
Transaction 一定是由外部账户签署的,账户到账户之间发送的 Message,要么它产生了一个合约,要么它是一个 Message Call,而且它可以激发合约之间越来越多的 Message Call。
再引用 Solidity 官方文档原文:
A transaction is a message that is sent from one account to another account (which might be the same or the special zero-account, see below). It can include binary data (its payload) and Ether.
In fact, every transaction consists of a top-level message call which in turn can create further message calls.
对于已经被创建好的合约而言,它收到的 transaction,都是 Message Call,对于很多其他文献而言,Message Call 也叫 internal transaction,但它不像正式的 transaction 一样带有签名,也就无法被 transaction api 查询出来。
我们经常讲的 call 和 transaction 还有是两种调用合约的方式:
- SendTransaction 必然会对链上数据进行修改。所以高层 API 通常只是得到一个 transaction hash,以后再得到 receipt
- call 则当场得到结果,但不会引起链上的数据变化
3.3 随机数问题
值得注意的几篇文章:
这个 reddit 上的帖子 里提到了 RANDAO 其实是不够安全的,但下面 RANDAO 的作者又出来说这个东西被它改进过了。
这个话题下面还有人引了 Vitalik 的一篇博客。
RANDAO 的实现。基本上就是用一个 dao 的方式(Decentralized autonomous organization)来运行一个匿名先知组织。这个设计思路和 Vitalik 谈到的用先知而不是全上链的版本来运行智能合约的对比基本一致。
vdice 自己的博客里也提到了用未来的块 hash 来生成随机数是不安全的,他们直接使用了 oraclize。改天要分析下它们所谓的"200 行的安全的 codebase"。
3.4 硬分叉
有四次计划内的软件升级,每次都是硬分叉:Frontier、Homestead、Metropolis、Serenity。
有一次意料之外的分叉(DAO 事件),制造出 ETH 和以太经典两种货币。
每次分叉都会造成矿工的迁移。旧链会因为流失算力而丧失安全性。
大都会分叉本来打算引爆难度炸弹,迫使矿工们从 PoW 共识算法移动到 PoA 共识算法,让以太坊进入冰河时代。但这个难度炸弹的引爆被延后了。
大都会同样引入了一个 PoS 的早期实施,Casper 共存算法允许每一百个区块里会有一个 PoS 区块。关于 PoS 算法,Vitalik 的解释是:
想象现在有 100 个人围着圆桌,其中有一个人拿着很多张纸,每张纸记录着很多笔历史交易信息。第一个人拿起笔签完后递给第二个人,第二个人也做出了相同的选择,如果大多数人做出了相同的选择,即都签署了同一张纸那么每一个参与者会获得 1 美元,当你做出和绝大多数人不同的选择时,那么你的房子就会着火!
如果真的不能阻止矿工停留在 PoW 上继续挖矿,那将会创建三种以太坊币:ETC、ETH-PoW、ETH-PoS,这对以太坊绝对是个噩梦!因为那不仅会降低以太坊的可信度和经济价值,还会稀释整个系统的哈希值比例,使得它更容易被黑客攻击!但如果能够把矿工移动到 PoS 网络上去,ETH 可以保持一致(通过新的账户映射做到?),不会有新的以太币。
挖出每个区块的奖励从 5 个 ETH 减少到 3 个。
3.5 谢灵点(Schelling Point)在以太坊中的应用
Focal point 或者 Schelling point 是博弈论中的一个概念,指的是人们在缺乏沟通的情况下,倾向于使用的解。因为人们拥有一样的常识,所以这些解对他们而言显得特殊、自然或者与他们有关系。这个观点最早是由美国诺贝尔经济学奖得主 Thomas Schelling 提出的。
警察系统在这几个世纪中已经不自觉地使用这个理论很久了。他们经常把犯人分开审问某件事的具体细节,囚犯想要说得一致以得到释放,唯一的可能就是说真话。
在以太坊中,也有利用谢灵点理论的变种谢灵币来达到一个公允的 data feeds 的实践,其简要的工作过程大致是:
- 所有人在第一个区块提交一个 value hash
- 所有人在接下来的一个区块提交 value
- 对 value 进行排序,在 25 分位和 75 分位之间的数给予奖励
这种机制可以做到一个类似预言机的机制:所有人都会尽量提供一个真实值,比如某地的温度,某天的物价。
这个机制要正确运行,防女巫攻击(sybil attack),要运用 PoW 和 PoS 机制才行。当然,这始终不是百分之百可靠的,还是可能有串谋机制。
四、为什么以太坊不适合做联盟链
联盟链必然要求多个账户系统存在,联盟中的每个节点都必须独立保存自己的私钥,则在当前的 gas 系统限制下,每个账户必须有自己的 ether 存款。
是不是允许多头出块?如果允许多头出块,则各个账户可以预先 prefund 或者在网络启动的时候充钱,不必考虑货币流通性问题。但多头出块的缺点是,不可抵挡分叉。而且,实际上极有可能还是存在货币流通性问题。
不允许多头出块,则必须由我们自己的中心账户来出块,我们自己来出块的话,其他账户发起合约请求需要的货币需要定期从我们的中心账户提取出来。
如果可以用强一致性的协议来预先持久化所有的写消息,也许可以靠监控把错误恢复过来,当然这也对业务产生了强依赖,业务的写操作必须是可以通过类似反幂等的方式恢复过来的。这就是把 Raft 分布式强一致性协议当做一个分布式的 WAL 来用了。ES 不适合拿来当这个 WAL,因为它不是强实时写的。
五、实战部署:CentOS 6.7 上 geth 搭建以太坊私链网络
5.1 环境准备
生成基本的路径:
1 | |
如果 git 版本是 1.x,卸载旧的 git,安装最新版的 2.x 以上的 git:
1 | |
执行以下命令,编译以太坊客户端:
1 | |
如果有 “fatal: unable to access ‘https://github.com/ethereum/go-ethereum/’: Failed connect to github.com:443; Operation now in progress” 考虑是容器的外网访问权限问题。
把 export PATH=$PATH:/root/go-ethereum/build/bin 加入环境变量:
1 | |
5.2 生成账户
生成一个叫 datadir 的文件夹来存储账户的私钥和链数据。
生成一个内容为 123456 的 password.txt。
用这个命令生成一个初始账户:
1 | |
查看一下生成文件的内容,我们可以看到如下内容:
1 | |
使用 ll 也可以查看到相似内容:
1 | |
5.3 创世区块配置
生成一个私链创世区块配置文件,gas limit 不能超过 53 bit,也就是 13 个 f,不然会出现 admin 连不上,合约出问题等种种麻烦事:
1 | |
其中:
- chainID 是接下来要用到的网络 id
- alloc 是为了链初始化的时候就先充值的账户
- coinbase 是这个节点附带的矿工的私人账户,可以写不正确的账户,反正节点启动的时候,会试图使用当前 keystore 里面的第一个账户
- difficulty 是这个网络的难度,数字越小,出块的时间越快。据 gitter 上的人说,实际上区块链网络会自动调整网络难度
- extraData 一个个性签名,目前还是用 16 进制的数好,不要用中文
- gasLimit 当前网络中一个区块可以容纳的 gas 的总数
5.4 初始化链
1 | |
5.5 启动节点
启动节点并进入控制台,这个命令可以重复执行,而且可以加上 --verbosity 6 来获取细节的输出:
1 | |
注意,这里的 --networkid 必须和 genesis.json 里的 chainid 相同,否则,可能覆盖前者。
查看当前节点信息:admin.nodeInfo
查看当前到底有多少个账户,可以看到预先生成的账户以及导入了:
1 | |
在 console 查看第一个账户的余额:
1 | |
查看所有账户余额的函数:
1 | |
在 console 上查看区块数量:
1 | |
大部分是空区块的话,一个空区块大概需要 1-2 kb 的磁盘空间。按 2 kb 一个空区块来看,一年 500 万个空区块大概需要消耗 10 g 的硬盘空间。
5.6 挖矿
在 console 上开始挖矿:
1 | |
每挖出一个区块来,有 5 个以太币的奖励。公网有差不多 9000 万个以太币。
5.7 多节点组网
在 node2 上把之前的动作(除了与账户相关的动作)都重做一遍。然后试图重建同一个账户节点的内容:
1 | |
如果没有相关的目录结构就 mkdir,如果有的话,就直接 touch,然后把 node1 的相同文件下的信息写进去。正如 geth 的官方文档所说:“It is safe to transfer the entire directory or the individual keys therein between ethereum nodes.”
在 node2 上启动区块节点控制台,在控制台上使用如下得到 node2 的节点信息:
1 | |
注意,把 @ 和 :30303 之间的地址换成公网可以访问的 IP 地址。
在 node1 的控制台执行以下命令:
1 | |
其实可以考虑使用 bootnode,如果 bootnode 不挂的话,倒是有点像超级账本里的 anchor peer。
如果两边都开始挖矿了,可以通过以下两个命令查看网络内的节点:
1 | |
两边都开始挖矿,block 会逐渐同步到 canonical chain。
gas 的计算公式:cost = gas * gasPrice,(账户1减少的资产 - 账户2增加的资产)/ gasPrice = 消耗的 gas
要注意,如果节点重启,peers 有可能会丢失,大家又会在各自的区块链上挖矿,直到重新 addpeer 然后同步,也有自动同步的例子。而且,节点之间的 account 是不能同步的。必须要拷贝 account 的 keystore 文件才行。
5.8 转账
尝试着转账:
1 | |
5.9 查询挖矿统计
在这个函数里,可以确认最近 N 个区块里有多少区块是由这个矿工挖出来的:
1 | |
获取当前的 gas limit:
1 | |
结果总是 4712388,不管 genesis.json 里面设置得多高。
删除链数据而保留账户数据:
1 | |
5.10 静默挖矿(nohup)
在退出 console 后,用挖矿模式 nohup 静默挖矿:
1 | |
5.11 查看可用模块
可以用如下命令查看当前到底有多少可用的 module:
1 | |
可以用 modules 查看到到底打开了多少个 rpc api:
1 | |
可以用 modules 查看到到底打开了多少个 rpc api:
1 | |
通过本地端口 attach 上去:
1 | |
5.12 安装高版本 nodejs
在本地安装高版本的 nodejs:
1 | |
目前已知的账户是:bbb
5.13 bootnode 节点
启动 bootnode 节点,让大家都去连 bootnode:
1 | |
5.14 重启矿工节点的脚本
从零开始重启动整个矿工节点的脚本:
1 | |
5.15 clique 算法(PoA)
使用 clique 算法(puppeth 生成出来的 genesis.json):
1 | |
从日志里读到事务 hash,找到这个事务的输入数据:
1 | |
六、实战部署:CentOS 6.7 上安装并使用 web3
6.1 编译环境准备
不要使用默认 gcc,会编译安装 web3 失败。
1 | |
不能使用全局安装,要尽量本地安装:
1 | |
6.2 web3 模块构成
web3 本身是一系列 nodejs 模块的集合,包括但不限于:
- web3-eth 针对以太坊区块链和智能合约
- web3-shh 针对耳语协议在 p2p 网络中的通信和广播
- The web3-bzz 针对 swarm 协议,去中心化的存储
- The web3-utils 针对 Dapp 的有用的功能
6.3 初始化 web3 对象
在 nodejs 中引入和初始化 web3 对象:
1 | |
6.4 示例合约:Calc
要使用的智能合约:
1 | |
6.5 基本操作
1 | |
6.6 完整合约调用示例
Calc 的相关 abi 和地址,并以此产生合约调用:
1 | |
6.7 最终版本的 calc-node.js
1 | |
用以下命令来 nohup 执行脚本:
1 | |
6.8 package.json
需要使用的 package.json:
1 | |
6.9 ab 压测
用 ab 进行 benchmark:
1 | |
6.10 运维须知
要千万小心,重启了 node 节点以后,要重新 unlock account,不然还是连不上。koa 应用程序不需要重启。
1 | |
七、实战部署:在以太坊网络上使用智能合约 solidity
因为一个并不周知的 issue,geth 客户端将不再提供 solc 编译相关功能。我们必须借助外部编译器,比如 solc/remix。
所谓 Contract,只是 Martin Fowler 的书里面经常提到的一个富血的类型罢了。
7.1 安装 solcjs
注意,要用高版本的 npm,来安装 solc:
1 | |
智能合约代码:
1 | |
用 solcjs 来编译代码:
1 | |
它会产生 testContract_sol_TestContract.bin 和 testContract_sol_TestContract.abi。结尾应该是 Contract 的类型。
7.2 解锁账户
使用以下命令先解锁账户:
1 | |
7.3 部署合约
在控制台里使用以下命令生成新的合约:
1 | |
注意看,address 还没有被 transaction 写入确认。如果挖矿了,最终它会被部署上去。
7.4 调用合约
用两种方式来调用智能合约:
1 | |
sendTransaction 的文档在这里。
查看当前合约变量的地址:
1 | |
通过地址反推代码:
1 | |
7.5 在其他节点定位合约
用一个典型的方式来在其他节点定位到这个合约:
1 | |
八、研究资料汇总
- 《以太坊的 gas 费率一览表》
- 《以太坊学习笔记:私有链搭建操作指南》
- 《以太坊中的账户、交易、Gas和区块Gas Limit》
- StackOverflow 上的问答:以太坊主链到底需要多大空间?
- StackOverflow 上的问答:怎样提供无限次数的智能合约操作?
- 《区块链技术-智能合约-以太坊 (译文)》
- 《以太坊官方文档》
- 《以太坊私有链搭建指南》
- 《以太坊关于搭建私有网络的 wiki》
- 《预充值以太坊资金的方法》。注意看 carchrae 的回复,这里面也提供了拷贝私钥复用私钥的方法,可以考虑在多节点的情况下使用
- 《一本与参数有关的介绍怎样搭建私链的 gitbook》
- StackOverflow 上的问答:以太坊的网络难度是否可以静态锁死?注意看它还有个相关的子问题。如果网络算力的稳定的话,应该不会出现难度增长才对
- 值得大读特读的 geth 的文档。特别是挖矿、账户管理的部分
- geth 的命令行选项。注意,有些选项在当前版本中已经消失了,如(gpomin、gpomax)
- StackOverflow 上的问答:如何降低测试网络中的难度。感觉没多大用
- 搜索以太坊上运行的 dapps 的网站
- solidity 官方文档里关于生成合约的部分代码其中提到了 “web3.eth.Contract to facilitate contract creation.” 是最佳实践
- 计算 gas 的一个例子。总算好懂一点了
- 查看以太坊的智能合约列表的网站
- 一个以太坊探测器的安装教程,安装区块链私链的例子,Truffle console 的例子(重要 )
- 以太坊的拥抱者例子
- 以太坊的自定义货币的例子,注意看里面设定货币总量的部分,和智能合约收费的部分
- 以太坊闹钟的例子
- 一个 web3js 编写调用合约的例子
- 如何学习 solidity
- StackOverflow 上的问答:用 call 和 send 来预写入、准写入区块链。类似超级账本的多重事件订阅
- 以太坊的名词解释
- 以太坊中 gas 和 log 的关系
- StackOverflow 上的问答:以太坊如何从外部世界获取数据,介绍 Oraclize 服务
- blockcypher
- Solidity 拷贝 memory 内容到 storage 的问答
- remix 的部署和测试环境的用法
- 使用 PoA 算法的私有链配置方法
- 用折衷的方法来升级以太坊合约
- 开源的以太坊钱包项目地址





