区小升 2019-11-18
如果 Layer 1 的关注点应该是状态而不是计算,在设计 Layer 1 区块链时,我们就需要先理解什么是区块链的状态。理解了状态是什么,我们才能理解状态爆炸是什么。
区块链网络中的每一个全节点,在网络中运行一段时间之后都会在本地存储上留下一些数据,我们可以按照历史和现在把它们分为两类:
共识协议的作用是通过一系列的消息交换,保证每一个节点看到的当前状态是相同的,而实现这个目标的方式是保证每一个节点看到的历史是相同的。只要历史相同(即所有交易的排序相同),处理交易的方式相同(把交易放在相同的确定性虚拟机里面执行),最后看到的当前状态就是相同的。当我们说「区块链具有不可篡改性」时,是指区块链历史不可篡改,相反,状态是一直在变化的。
有趣的是,不同的区块链保存历史和状态的方式不同,其中的差异使得不同的区块链形成了各自的特点。由于这篇文章讨论的话题是状态,而影响状态的历史数据主要是交易(而不是区块头),接下来的讨论历史的时候会侧重交易,忽略区块头。
Bitcoin 的状态,指的是 Bitcoin 账本当前的样子。Bitcoin 的状态是由一个个 UTXO(尚未花费的交易输出)构成的,每个 UTXO 代表了一定数量的 Bitcoin,每个 UTXO 上面写了一个名字(scriptPubkey),记录这个 UTXO 的所有者是谁。如果要做一个比喻的话,Bitcoin 的当前状态是一个装满了金币的袋子,每个金币上刻着所有者的名字。
Bitcoin 的历史由一连串的交易构成,交易内部的主要结构是输入和输出。交易更改状态的方法是,把当前状态中包含的一些UTXO(交易输入引用的那些)标记为已花费,从 UTXO 集合中移出,然后把一些新的 UTXO(这个交易的输出)添加到 UTXO 集合里面去。
可以看出,Bitcoin 交易的输出(TXO,Transaction Output)正是上面说的 UTXO,UTXO 只不过是一种处于特殊阶段(尚未花费)的 TXO。因为构成 Bitcoin 状态的组件(UTXO),同时也是构成交易的组件(TXO)。由此 Bitcoin 有一个奇妙的性质:任意时刻的状态都是历史的一个子集,历史和状态包含的数据类型是同一维度的。交易的历史(所有被打包的交易的集合,即所有产生过的 TXO 的集合)即状态的历史(每个区块对应的 UTXO 集合的集合,也是所有产生过的 TXO 的集合),Bitcoin 的历史只包含交易。
在 Bitcoin 网络中,每一个区块,每一个 UTXO 都要持续占用节点的存储空间。目前 Bitcoin 整个历史的大小(所有区块加起来的大小)大约是200G,而状态的大小只有大约 3G(由约 5000万个UTXO组成)。Bitcoin 通过对区块大小的限制很好的管理了历史的增长速度,由于其历史和状态之间的子集关系,状态数据大小必然远小于历史数据大小,因此状态增长也间接的受到区块大小的管理。
Ethereum 的状态,也叫做「世界状态」,指的是 Ethereum 账本当前的样子。Ethereum 的状态是由账户构成的一棵 Merkle 树(账户是叶子),账户里面不仅记录了余额(代表一定数量的 ether),还记录了合约的数据(例如每一只加密猫的数据)。Ethereum 的状态可以看作是一个大账本,账本的第一列是名字,第二列是余额,第三列是合约数据。
Ethereum 的历史同样由交易构成,交易内部的主要结构是:
交易更改状态的方法是,EVM 找到交易发送的目标账户:
1.根据交易的 value 计算目标账户的新余额;
2.将交易携带的 data 作为参数传递给目标账户的智能合约,运行智能合约的逻辑,在运行中可能会修改任意账户的内部状态生成新的状态;
3.构造新的叶子存放新的状态,更新状态 Merkle 树。
可以看出,Ethereum 的历史和交易结构与 Bitcoin 相比有非常大的不同。Ethereum 的状态是由账户构成的,而交易是由触发账户变动的信息构成,状态和交易中记录的是完全不同类型的数据,二者之间没有超集和子集的关系,历史和状态所包含的数据类型是两个维度的,交易历史大小与状态大小之间没有必然的联系。交易修改状态后,不仅会产生新的状态(图中实线框的叶子),而且会留下旧的状态(图中虚线框的叶子)成为历史状态,因此 Ethereum 的历史不仅仅包含交易,还包含历史状态。因为历史和状态属于不同的维度,Ethereum 区块头中不仅仅包含交易的 merkle root,也需要显式包含状态的 merkle root。(思考题:EOS 使用了类似 Ethereum 的账户模型,却没有在区块头中包含状态的 Merkle Tree Root,这是好还是不好?)
Ethereum 中每一个区块,每一个账户都会持续占用节点的存储空间。Ethereum 节点在同步的时候有多种模式,在 Archive 模式下所有的历史和状态都会保存下来,其中历史包括历史交易和历史状态,所有数据加起来的大小超过了 2TB;在 Default 模式下,历史状态会被裁剪掉,本地只保留历史交易和当前状态,所有数据加起来大约是 170G,其中交易历史大小是 150G,当前状态大小是 10G。Ethereum 中所有的开销管理都被统一到 gas 计费模型之下,交易的大小需要消耗对应的 gas,而每一条 EVM 指令消耗的 gas,不仅考虑了计算开销,也将存储开销考虑在内。通过每个区块的 gaslimit,间接限制了历史和状态的增长速度。
ps. 常见的一个误解是:Ethereum 的「区块链大小」已经超过 1T 了。从上面的分析我们可以看到,「区块链大小」是一个非常模糊的定义,如果把历史状态算进去,它确实超过了,但是对于全节点来说,把历史状态删掉没有任何问题,因为只要有 Genesis 和交易历史,任意时刻的历史状态都可以重新被计算出来(不考虑计算需要的时间)。真正有意义的数据,是全节点必须的数据的大小,Bitcoin 是 200G,Ethereum 是 170G,两者是基本相同的,而且在平均配置的云主机上都能装下,因此人们观察到的 Ethereum 全节点减少 并不是由于存储增加导致的(根本原因是同步时的计算开销,这里不展开了)。考虑到 Ethereum 的历史长度(当前区块的 timestamp 减去 genesis 的 timestamp)不到 Bitcoin 的一半,可以看出 Ethereum 的历史和状态大小增长更快。
The Tragedy of (Storage) Commons:区块链版本的公地悲剧
公地悲剧所指的是这样一种情况,有限的共享资源在不受任何使用限制的情况下会被人们过度消耗。区块链节点为保存历史和状态付出的存储,正是这样一种共享资源。
区块链节点为处理交易所花费的资源有三种,CPU、存储和网络带宽。CPU 和带宽都是每个区块会刷新的资源,我们可以认为每个区块间隔内都有同样多的 CPU 和带宽可供使用,上个区块消耗掉的 CPU 和带宽不会让下个区块可用的 CPU 和带宽变少。对于可刷新的资源,我们可以通过一次性支付的交易手续费来补偿节点。
与 CPU 和带宽不同,存储是一种占用资源,在一个区块中被占用了的存储,除非使用者主动释放,否则无法在后面的区块中被其它使用者使用。节点需要为存储持续的付出成本,而使用者却不需要为存储持续的支付手续费(记住交易手续费只需要支付一次)。使用者只需要在往区块链写数据的时候支付一点点手续费,就可以永久使用一个可用性超过 Amazon S3 的存储,其无限大的永久存储成本需要区块链网络中的所有全节点来承担。
Ethereum 上由于各种 DApp 的存在,The Tragedy of (Storage) Commons 相对更加严重。例如,在区块 5700001(May 30, 2018)的时候,使用状态最多的 5 个合约是:
1.EtherDelta, 5.09%
2.IDEX, 4.17%
3.CryptoKitties, 3.05%
4.ENS, 1.92%
5.EOS Sale, 1.73%
比较有趣的是最后一个,EOS Sale。虽然 EOS 的众筹已经完成,EOS 代币已经在 EOS 链上流转,EOS 众筹的记录却永远留在了Ethereum 的节点上,消耗 Ethereum 全节点的存储资源。
可以看到,在缺乏管理的情况下,区块链的存储资源会被有意或者无意的滥用。在一个设计合理的经济模型中,使用者必须承担存储占用的成本,这个成本不仅仅与占用存储空间的大小成正比,还与占用时间的长度成正比。
无论是历史还是状态数据都会占用存储资源。通过上面对 Bitcoin 和 Ethereum 的分析(其他区块链的状态模型基本都可以归纳为二者之一)可以看到,虽然它们对历史和状态的增长进行了管理,但是对历史和状态的总大小却没有任何控制,这些数据会持续无休止的累积下去,使得运行全节点需要的存储资源越来越大。提高全节点的运行门槛,使网络的去中心化程度越来越低,这是我们不愿意看到的。
你也许会说,有没有可能硬件平均水平的提高会超过历史和状态的积累速度?我的回答是可能性很低:
从这张图中我们可以看到,随着 Ethereum 网络的发展,状态数据累积的数量呈指数式的增长。Bitcoin 的状态数据从 0 积累到 3G,用了 10 年;Ethereum 的状态数据从 0 积累到 10G,用了 4 年;而这是在我们还没有解决 Scalability 问题,区块链仍然是小众技术的情况下的增长速度。当我们解决了 Scalability 问题,区块链真正获得 mass adoption,DApp 和用户数量都爆炸式增长的时候,区块链历史和状态数据会以什么速度累积呢?
这就是状态爆炸问题,我们把它归类为 post-scalability problem,因为它在解决 Scalability 问题之后会非常明显。我们最早是在做许可链场景落地时注意到了这个问题,因为许可链的性能远高于公有链,刚好处于 post-scalability 的阶段。(思考题:许可链怎么解决状态爆炸问题?)
历史数据的累积相对容易处理,未来可以通过去中心化的 Checkpoint 或是零知识证明等技术来压缩,在那之前全节点甚至可以把历史直接丢掉,依然可以正常运行。状态数据的累积则麻烦许多,因为它是全节点运行必须的数据。
不少区块链项目已经看到了这个问题,并提出了一些解决方案。EOS RAM 是解决状态爆炸问题的一个有益尝试:RAM 代表了超级节点服务器可用的内存资源,无论是账户、合约状态还是代码,都需要占用一定的 RAM 才能运行。RAM 的设计也有很多问题,它需要通过内置的交易市场购买,不可转让,无法租用,将合约执行过程中的短期内存需求和合约状态的长期存储需求混在了一起,而且 RAM 的总量设定没有确定的规则,更多取决于超级节点可以承受的硬件配置,而非共识空间的成本。
Ethereum 社区也看到了这个问题并提出了 Storage Rent 的方案:要求使用者为存储资源的使用预支付一笔租金,占用存储资源会持续消耗这笔租金,占用时间越长,使用者需要支付的租金越多。Storage Rent 方案存在两个问题:
1.预支付的租金终有一天会用完,这时候如何处理占用的状态?正是为解决这个问题,Storage Rent 需要诸如 resurrection 的机制来补充,增加了设计的复杂度,使智能合约的 immutability 大打折扣,也为使用体验带来了麻烦;
2.Ethereum 的状态模型是一种共享状态的模型,而不是 First-class State。以 ERC20 Token 为例,所有用户的资产记录都存放在单个 ERC20 合约的存储里面,在这种情况下,应该由谁来支付租金?
解决状态爆炸问题也是 Nervos CKB 的设计目标之一,为此 CKB 走了一条完全不同的、更为彻底的变革之路。