区块链与状态爆炸

如果Layer 1的关注点应该是状态而不是计算, 在设计Layer 1区块链的时候,我们就需要先理解什么是区块链的状态。理解了状态是什么,我们才能理解状态爆炸是什么。

状态

区块链网络中的每一个全节点,在网络中运行一段时间之后都会在本地存储上留下一些数据,我们可以按照历史和现在把它们分为两类:

  • 历史 - 区块数据和交易数据都是历史,历史是从Genesis到达当前状态的路径。
  • 状态(即现在) - 节点在处理完从Genesis到当前高度的所有区块和交易后形成的最终结果。状态随着区块的增加一直处于变化之中,交易是造成变化的原因。

共识协议的作用是通过一系列的消息交换,保证每一个节点看到的当前状态是相同的,而实现这个目标的方式是保证每一个节点看到的历史是相同的。只要历史相同(即所有交易的排序相同),处理交易的方式相同(把交易放在相同的确定性虚拟机里面执行),最后看到的当前状态就是相同的。当我们说“区块链具有不可篡改性”的时候,指的是区块链历史不可篡改,相反,状态是一直在变化的。

有趣的是,不同的区块链保存历史和状态的方式不同的,其中的差异使得不同的区块链形成了各自的特点。由于这篇文章讨论的话题是状态,而影响状态的历史数据主要是交易(而不是区块头),接下来的讨论历史的时候会侧重交易,忽略区块头。

举个例子:Bitcoin的历史和状态

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账本当前的样子。Ethereum的状态是由账户构成的一棵Merkle树(账户是叶子),账户里面不仅记录了余额(代表一定数量的ether),还有合约的数据(例如每一只加密猫的数据)。Ethereum的状态可以看作一个大账本,账本的第一列是名字,第二列是余额,第三列是合约数据。

Ethereum的历史同样由交易构成,交易内部的主要结构是

  • to - 另一个账户,代表交易的发送对象
  • value - 交易携带的ether数量
  • data - 交易携带的任意信息

交易更改状态的方法是,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和带宽变少。对于可刷新的资源,我们可以通过一次性支付的交易手续费来补偿节点(手续费与计算复杂度和交易大小的相关性可参考RFC0015 Appendix)。

与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走了一条完全不同的、更为彻底的变革之路。

17 Likes

读这篇文章的收获好多呀,不知道有没有人想把这篇文章翻译成英文
征求志愿者将此文翻译成英文阿~加我 wechat: chainhenry

1 Like

在许可链里面,每个节点都是可信的,所以一个节点是不是只要存储和它自身相关的数据状态就可以了,而不需要同步许可链里面所有的全球状态,这等于做了一个变相的分片。这样可能就在一定程度可以上缓解状态爆炸的问题。

但是如果假设一个极端的情况,许可链里面就两个节点,然后他们之间都面临状态爆炸的情况,那可能就需要通过别的方式去解决状态爆炸的问题了。状态数据压缩,根据状态修改的不同频率进行分级?

其它节点如何验证不在自身存储的数据呢?

就是一份数据状态不用所有全部节点存储,只存储在一部分节点中,不是自身存储的数据就直接向别的节点索取,因为是许可链,数据验证是不是就可以弱化了

1 Like

以太坊分片之后,每个节点还是需要存储所有片上的全球状态么?

诶,我理解以太坊的分片是交易分片,不是存储上的分片,为了提升交易速度的方案。许可链里面的节点也不一定都是可信的,不然也没有必要用BFT的共识。
一个节点只存储于自身相关的数据一般出现在隐私方案里,但是存在其它节点无法验证这笔交易的问题。
解决许可链状态爆炸,理解至少有两种方法。
一个是许可链可以根据业务需求,对保存时间做限定,比如一般商业系统的数据保存时间是2年,在2年以外的数据做冷存贮。
二个是做快照,把历史状态删除。保留历史块交易信息。
不知道楼主是否有其它更好方案 @janx

谢谢解答。其实我理解的这个问题是,当前全球状态的爆炸怎么办?
对于历史状态和历史交易数据我们可以进行删除或封存处理,但是冷存储和历史状态删除都不会对现有的全球状态的大小造成太大影响。

比较好奇的是,比特币的3G和以太坊的10G的全球状态数据,在主链性能大大大幅度提升的状态下,接近于状态爆炸的情况,这个时候该怎么办?

是我想的比较简单吗,对于 Jan 的这个问题:

用更牛逼的硬件设备就完事了=。= :rofl:
不过如上面所说,可以采用:

毕竟联盟链的安全性在于运行联盟链的节点自身的信誉,个人觉得历史数据没有那么重要?

另外,以太坊 2.0 的方案是有一个 Beacon Chain 作为主 shard,作为所有分片交易安全的锚点,分片会存储自己交易和状态并且定时 commit 到 Beacon Chain 里。但是我看到跨分片交易就非常复杂,需要分片 A 、B 、Beacon Chain 来回 commit 进行确认保证安全性,在考虑 finality 的情况下,据估计一次交易 24 分钟左右(主要是 Beacon Chain 上面需要确认两次)。

历史交易和历史状态,可以在全部节点达成统一意见之后进行快照然后删掉,但是如果全球状态本身处于爆炸状态是不是一点办法都没有?(除了让所有人用更牛逼的硬件设备?)

有些人会认为全部的历史交易还是非常重要的,因为后面加入的节点也可以通过历史交易进行全部的状态验算,但是我觉得在达成统一共识的状态下,从0开始和从10000开始并没有那么大的区别吧。

联盟链的话,不需要考虑这些吧…如果联盟链还有问题那就是联盟的问题…

联盟链有私钥保护节点身份,且 PBFT 类共识不会分叉,所以全节点对网络安全性不是很重要(联盟链不会有分叉攻击、伪造块等等)。

联盟链可以放弃全节点,定期持久化最新的 block hash 和状态摘要作为锚点,删除所有历史块。

历史和状态不是一个东西吧?
为啥楼上都在说裁剪历史,还是说我搞错了?

联盟链解决状态爆炸问题:
直接联盟所有节点对状态增加收费呀,用户增加一个状态该付多少手续费,直接所有节点平分。

既然只有有限个节点,那每笔交易消耗多少带宽/计算/存储这三种资源应该是能直接算出来的东西。
所以直接按交易收费,然后费用分给所有节点就可以了,不是吗?

状态的增长速率受 (1) system constraints(rent、block gas limit)和 (2) growth rate of adoption,这两方面因素影响。

若考虑到未来真正的 mass adoption,则 (2) 的作用应该远远超过 (1)。所以未来真到这一步的话,全节点不可避免地还是要扩存储容量的,要么垂直扩容(增大磁盘容量),要么水平扩容(分布式存储)。
这么想的话,又觉得无论采取什么设计,状态爆炸都是不可避免的问题。

1 Like

根据三角理论,许可链舍弃了去中心化,那么它就理所应当的应该获得性能上的提升

同意。我在想对状态存储收费,会促进状态的转移,状态的转移也是状态存储的一部分,一样会占用状态空间啊。即使注销了,那历史的阴影还会在啊。

我认为会对只存储最新状态的规模缩减有好处

那只能加硬盘了

那EOS sync的时候只能重新计算?

发一笔交易,不论状态生成还是状态转移,用多少资源就收多少费呗。
就像楼上说的,硬盘不够就加硬盘。

重要的是联盟链里就不存在一个公地问题了。
公链有公地问题是因为它是“公共” (permissionless)的,谁都能成为节点。

联盟链就不是“公共”的了,很大程度上是个私有的,所有节点付出的资源多少是可以算出来的。

联盟链就有点像阿里云了已经,只不过不是阿里云一方负担成本,而是好几方一起负担成本。

区块链

浅显的核心理解:以区块作为载体,将具有价值的数据进行存储,链接成为全网数据库,达到不可篡改的目的,进而实现价值的共识。
普遍的追求:去中心化,抗审查

状态

经过全网认可的数据集,不管是过去的,当下的,还是未来的。全网有意见不统一的,小到软分叉(协议升级),大到硬分叉,价值分裂,各自继续耕作在自己价值观的网络中。这些都是可能的。

  • 比特币UTXO系列:将整个交易集存储下来,然后复盘全网任何时刻的状态。(对比传统的数据库数据,不去存储数据历史的状态(status),只将整个数据的所有合法修改集进行保存(log)。
  • 以太坊Account系统:进行全网最终的认可的状态储存,而不存储交易过程。(对比传统的数据库数据,只存储当下合法数据操作以后认可的数据,而不存储数据的修改集。

状态爆炸的核心依然是为了维护去中心化的数据一致性问题。这里,试想,全网的状态能否通过某种技术和方法进行分发或者分布式存储(不影响矿工挖矿或者用户使用或者开发者构建生态),同时,提供途径进行全网数据的单份或者冗余份数统一,这样,状态爆炸带来的压力可以随着生态的扩大与之均衡化。

1 Like