在庞大的区块链行业中,“世界状态”是时常为人所提起的技术名词。世界状态究竟是什么,不同项目的世界状态是如何设计与实现的,状态爆炸又该如何解决?Ultrain 技术总监特此撰文,一一解答这几大疑问,欢迎大家阅读。

1. 什么是世界状态

区块链可以理解为一个分布式的状态机:所有节点从同一个创世状态开始,依次运行达成共识的区块内的交易,驱动各个节点的状态按照相同操作序列(增加,删除,修改)不断变化,实现所有节点在执行完相同编号区块内的交易后,状态完全一致,我们把这个状态称之为世界状态。

区块链技术咖口中的世界状态,究竟是如何设计与实现的?图 1 区块链状态改变
世界状态里面记录了各种信息,比如账户余额,智能合约字节码,各个智能合约自定义的数据(例如一个游戏 DAPP 的道具相关信息),链的配置参数等。世界状态表示的是当前状态,即记录的各状态数据的当前数值。为保证执行交易时能够快速地对世界状态进行更新,世界状态方案设计实现要考虑状态数据的快速查找以及高效更新。

2. 比特币的世界状态处理

比特币的世界状态数据存储于 chainstate 目录,采用 LevelDB 进行管理,主要存储当前还没有花费的所有交易输出以及交易的元数据信息,用来验证新传入的交易和块,存储这些数据的时候会做适当的压缩。
LevelDB 是一个可持久化的键值数据库,利用磁盘顺序写性能比随机写大很多的特性,采用 LSM-Tree 结构,将磁盘的随机写转化为顺序写,大大提高了写速度。LSM 将树结构拆成一大一小两棵树,较小的一个常驻内存,较大的一个持久化到磁盘。写入操作会首先操作内存中的树,随着内存中树的不断变大,会触发与磁盘中树的归并操作,而归并操作本身仅有顺序写。
Bitcoin 的 LevelDB 存储了多种数据,其中最主要的为 Coin 数据,以的方式存储,Key 是 CoinEntry 类型,由三部分组成:1 字节的大写字符“C”(DB_COIN),32 字节的交易 ID 值以及 4 字节的序列号;Value 为 Coin 对象序列化后的值。
区块链技术咖口中的世界状态,究竟是如何设计与实现的?图 2 比特币 LevelDB 存储的 Coin 数据
与 Coin 数据操作相关类的如下图所示:CCoinsView 接口类定义了对 Coin 的操作集合,CoinsViewDB 类用来真正与 LevelDB 交互,此类有一个全局实例 pcoinsdbview;CCoinsViewBacked 类作为多个 Coinview 层级之间的转接层;CCoinsViewMemPool 类专门用来处理 Mempool 相关的未花费交易;CCoinsViewErrorCatcher 类包装了对 LevelDB 读取的错误处理;CCoinsViewCache 类内部使用 CCoinsMap 存储了 CoutPoint 到 CCoinsCacheEntry 对象的映射,此类有一个全局实例 pcoinsTip。
CCoinsViewCache 类是一个内存缓存实现。获取 Coin 时,使用传入的 OutPoint 对象作为 Key, 在 CCoinsViewCache 内部成员 hashmap 中查找;如果找不到,则在 CCoinsViewCache 对象初始化时传入的 base view 中查找 , 如果在 base view 找到,则使用此条目填充内部 hashmap。添加 Coin 时,也是添加到 CCoinsViewCache 内部成员 hashmap,并设置相应的 DIRTY,FRESH 标记。CCoinsViewCache 有 Flush 接口,将缓存内容通过 BatchWrite 写入数据库。区块链技术咖口中的世界状态,究竟是如何设计与实现的?图 3 比特币 Coin 操作相关类

3. 以太坊的世界状态处理

比特币的世界状态是通过网络中全局的未使用交易输出(UTXO)来描述的,而以太坊则利用账户概念来描述状态信息。账户状态包含四个属性,nonce,balance,storageRoot 和 codeHash。以太坊用 stateObject 来管理账户状态,账户以 Address 为唯一标示,其信息在相关交易的执行中被修改。所有账户对象逐一插入 Merkle-PatricaTrie(MPT) 结构里,形成 stateTrie。区块头数据结构中的 Root 字段存储了 stateTrie 的 root 数值,即世界状态的哈希值。
区块链技术咖口中的世界状态,究竟是如何设计与实现的?图 4 以太坊世界状态哈希 以太坊采用 StateDB 来管理 stateObject,它包含两种接口:Trie 接口用于操作内存中的 MPT,Database 接口用于操作持久化存储数据库。以太坊不同版本的客户端使用的数据库不同,Go、C++以及 Python 客户端使用的是 LevelDB,用 Rust 实现的 Parity 客户端使用的是 RocksDB。RocksDB 基于 LevelDB 开发而来,优化了 LevelDB 存在的一些问题。
StateTrie 存储了账户的信息(余额,发起交易次数,虚拟机指令数组等等),因此每次交易执行都会导致 stateTrie 变化,StateDB 定义了一个函数 IntermediateRoot(),用来生成实时的 Root 值。交易执行的入口函数 StateProcessor.Process() 在返回前调用了 Engine.Finalize(),Finalize() 在内部调用上述 IntermediateRoot() 函数并赋值给区块头 Root 字段,该值就是在该区块所有交易完成后,所有账户信息的即时状态生成的 root 数值。
StateDB 内部利用两级缓存机制来存储和更新所有代表账户的 stateObject 对象,第一级缓存以 map 的形式存储 stateObjects;第二级缓存以 MPT 的形式存储,最终调用 CommitTo 接口实现键值数据库上的持久化存储。
区块链技术咖口中的世界状态,究竟是如何设计与实现的?图 5 以太坊世界状态更新
从比特币与以太坊的处理过程可以看到,两个项目均采用了内存缓存机制,配合实现持久化的键值数据库来管理世界状态,从而保证对世界状态更新的高效性。另外,两个项目无论内存缓存,还是底层持久化,均采用键值映射数据结构,以实现数据快速查找。以太坊还利用 MPT 结构在内存跟踪状态变化,并在每个 Block 的数据块头记录了其所对应的世界状态的哈希值,将各个节点的世界状态一致性也纳入到了共识校验范围。

4. Ultrain 世界状态处理

区块链的一个重要指标是交易处理能力 TPS(Transaction Per Second),交易处理过程会对世界状态做高速频繁读写,因此需要高性能数据库来管理世界状态。另外,交易执行对世界状态的修改,有可能因为共识协议要求进行回滚,所以需要数据库或应用层逻辑能支持数据修改回滚。比特币与以太坊所采用的键值数据库 LevelDB 或者 RocksDB 对简单 Key/Value 查询支持的比较好,但是对复杂查询逻辑支持需要应用层自行实现。Ultrain 实现的开放联盟链(以下简称开放联盟链)实现时综合考虑上述原因,选择了开源的 chainbase 数据库进行世界状态管理。
区块链技术咖口中的世界状态,究竟是如何设计与实现的?图 6 开放联盟链世界状态处理 chainbase 为内存数据库,采用内存文件映射技术,将文件映射到进程的地址空间,在物理内存空间够用的情况下,可以保证足够高的读写性能。chainbase 具有 undo session 概念,支持对未 commit 的状态修改进行回滚。chainbase 基于 boost 库的 multi_index_container 实现,支持多关键字索引,从而支持丰富的查询逻辑。

  • chainbase 内存映射

进程里的内存,可分为 file-backed 和 anonymous 两种:file-backed 有文件对应,且数据可修改,比如程序 TEXT 段,文件共享内存且数据可修改等;anonymous 内存里数据没有文件对应,或者对应文件里内容不可修改,比如堆,栈,共享库,静态数据区以及未初始化的数据区。
chainbase 使用 memap,并设置 MAP_SHARED 标志,该模式是 file-backed 类型。系统将文件映射到进程的地址空间,直接访问内存实现对文件的读写操作。系统定期将脏数据写入磁盘,会占用 CPU 和 IO,影响性能。chainbase 大小可以超过物理内存,当访问数据时,系统通过缺页中断将数据加载到内存。虽然理论上 chainbase 可以无限大,但数据量超过物理内存则会导致频繁的缺页中断,使交易执行时间变大,严重影响系统 TPS 性能。

  • chainbase 状态回滚支持

chainbase 支持 undo session 概念。当创建一个 undo session 之后,会对 chianbase 的增加,删除,修改操作进行跟踪记录。如果需要回滚,则在 undo 的时候还原原始记录;如果不需要回滚,则在 commit 的时候丢弃保存的 undo 状态信息使修改变得不可逆。
chainbase 内每张数据表都有三个独立 undo stack,分别记录对该数据表所做的增加,删除,修改操作。每个 Block 和每个 Transaction 开始都会创建一个新的 undo cache 条目;Transaction 成功,就执行 undo stack 融合操作,将修改并入 Block 的 undo 条目;交易失败则进行状态回滚,回退 Transction 对数据表的修改。
区块链技术咖口中的世界状态,究竟是如何设计与实现的?图 7 chainbase undo 机制
具体操作流程如上图所示,比如 chainbase 内原有变量 a 数值为 3,新到的交易将其修改为 4,chainbase 会在修改 undo statck 内记录原数值 3,如果交易成功,则将该修改记录与 block 层面的 undo stack 融合;如果交易失败,则执行回滚操作。Block 被 commit 确认后,undo stack 内容被丢弃。

  • chainbase 多索引支持

chainbase 采用 boost 库中的 multi_index_container 数据结构,它是 boost 实现的一个多关键字索引容器,可以理解为数据库中的表。在链化未来的实现中,chainbase 内核心数据表一共有二十多张(参见 controller.cpp 中 add_indices 函数)。与智能合约开发数据库操作相关的有两张表 table_id_multi_index 和 key_value_index,table_id_multi_index 保存着所有智能合约中创建的表信息,key_value_index 保存着所有智能合约中创建的表的数据记录。此外还有五张表(index64_index,index128_index,index256_index,index_double_index,index_long_double_index)用来存储二级索引数据,实现数据记录的灵活查询。

5. 世界状态快照生成

开放联盟链为主侧链架构,节点会随机在侧链间进行调度,链间调度引入了一个问题:节点从一条侧链调度到另外一条侧链,相当于一个新节点加入了该侧链网络,那这个节点需要有与其他节点一致的世界状态,才能加入共识参与出块流程。构建世界状态,可以利用历史区块数据,依次执行区块内保存的交易,重构出世界状态;也可以利用保存好的世界状态快照文件,直接恢复到指定块的世界状态,再继续重放后续区块。第一种方式中,链运行越久,产生的数据越多,重放需要耗费的时间越久;第二种方式中,一方面需要考虑如何能够高效地生成世界状态快照文件,另一方面要提供一种机制让节点可以验证其获取的世界状态快照文件是正确的没有经过篡改的。
如何高效地生成世界状态快照文件而不影响主线程正常处理交易,即不对 TPS 性能造成影响,Ultrain 采用了 cache 机制,并使用单独的世界状态快照生成线程。如下图所示,类似 chainbase 的 undo stack 机制,Ultrain 增加了 cache 模块。每次交易对 chainbase 数据的修改,一方面会进入 undo stack 用于支持状态回滚,另一方面会进入 cache 模块,用于给单独的世界状态快照生成线程处理。cache 模块的处理逻辑与 undo stack 不同的地方在于,cache 会保留多个 block 数据块的修改,并不断进行融合,每隔一定间隔(比如每生产 100 个区块)生成一个新的条目,如下图中 901~1000,表示为编号为 901 到编号为 1000 的区块之间的数据修改 cache。
区块链技术咖口中的世界状态,究竟是如何设计与实现的?图 8 undo stack 与 worldstate cache 类似 LevelDB 中内存与文件系统文件融合的机制,开放联盟链的世界状态快照生成过程中会将世界状态数据存储到文件系统,利用 cache 机制,内存中仅占用少量空间来记录一小段时间内的数据修改。到特定区块间隔(比如每 100 块区块),世界状态生成线程(下图中的 ws thread)会逐表将数据加载进内存(下图中的 backup table db),并与 cache 中的修改记录进行融合,然后重新写回到文件系统。采用该机制,使得世界状态快照生成过程既不会影响主线程性能,也不会产生内存的大量占用。
区块链技术咖口中的世界状态,究竟是如何设计与实现的?图 9 主线程与世界状态生成线程
文件系统每隔一定区块(可配置参数,目前链化未来主网配置为 1000 块)对生成的世界状态快照文件进行单独保存,并计算快照文件的哈希值。各个节点会将该哈希值上报到主链系统合约,被调度的节点启动时,会从主链获取该哈希值,从而使节点可以校验获取到的世界状态快照文件是否被人恶意篡改过。
世界状态快照生成机制,保证了节点可以快速恢复到指定区块世界状态,从而支撑链化未来主侧链机制中关键的节点随机调度功能。

6. 状态爆炸及解决思路

区块链网络中的全节点在运行一段时间后,会在本地存储产生越来越多的数据。本地存储的数据包括历史数据和世界状态数据:历史数据主要以区块的形式存储,区块内包含了所有的交易信息;世界状态数据为节点处理完从创世块开始到当前块高包含的所有交易形成的当前状态数据。
无论是历史数据还是世界状态数据,都会随着系统的运行持续增长,使得运行全节点需要的存储资源越来越大。当前区块链的处理性能还比较低(比特币为 7TPS,以太坊为 15TPS,其他区块链一般为数千 TPS),考虑到区块链处理性能获得大大提升后,存储增长问题会变得更严重,这就是状态爆炸问题。
历史数据的积累相对比较容易解决,可以采取中心化压缩存储,或者利用 Checkpoint 机制,全节点可以不用存储某个时间点之前历史数据。对于世界状态数据的状态爆炸问题,目前主要有状态租赁方案,状态剪枝方案,“无状态节点”方案等解决思路。

  • 状态租赁方案

状态租赁方案要求用户为存储的数据支付租金,该租金不仅考虑数据占用空间大小,而且与其在链上存储的时间成正比。通过让用户支付状态费用,可以推动不再使用的状态信息可以随着时间的推移而被删除,从而防止数据逐步增长。关于收费模式有不同的讨论,目前没有明确的结论。

  • 状态剪枝方案

状态剪枝方案是指全节点只存储近期的数据,比较老的历史数据将存储在其他地方,状态剪枝方案会影响那些依靠全节点索引和查询所有历史数据的 DApp。状态剪枝方案不能从根本上解决问题,而且随着区块链应用越来越多,数据增加的速度也越来越快,由于存储空间不变,那么能被存储的近期数据时间也就会越来越短。

  • “无状态节点”方案

在无状态节点方案中,全节点不需要存储区块链的状态,而只需要对状态进行短暂的承诺(Commitment)以验证交易,这需要利用到的密码学原理包括累加器和向量承诺(Accumulators and Vector Commitments)。有研究机构构建了基于比特币和以太坊的概念验证模型,但是从概念验证到真正的落地应用,还需要做大量的工作。
针对状态爆炸问题,还有项目提出了一些新的思路,比如 EOS 内置交易市场,用户需要支付原生代币购买 RAM 进行世界状态存储;Nervos 采用 Cell 模型,利用原生代币经济模型激励存储空间自由定价交易;Coda 利用零知识证明的不断递归实现存储压缩技术等。
Ultrain 所采取的主侧链结构中,各个侧链的世界状态相互独立,可以理解为对世界状态的水平切分,缓解了单链结构中状态爆炸的问题。此外,Ultrain 也在尝试节点状态压缩技术,即节点只保存部分世界状态数据,结合交易提供的状态数据完成交易执行。同时 Ultrain 也在积极探索“无状态节点”技术,争取早日实现该技术的工程落地。
让信任计算赋能各行各业

区块链技术咖口中的世界状态,究竟是如何设计与实现的?Ultrain 团队核心管理人员

区块链技术咖口中的世界状态,究竟是如何设计与实现的?

扫二维码下载 UltrainOne APP:

区块链技术咖口中的世界状态,究竟是如何设计与实现的?


以下为 Ultrain 各社交媒体账号
欢迎大家加入:

区块链技术咖口中的世界状态,究竟是如何设计与实现的?微信用户可关注公众平台:ultrainchain 并可添加管理员微信(ultra-in)或扫码申请加入超脑链 Ultrain 中文社区区块链技术咖口中的世界状态,究竟是如何设计与实现的?

区块链技术咖口中的世界状态,究竟是如何设计与实现的?微博用户可关注:@Ultrain 超脑信任计算

区块链技术咖口中的世界状态,究竟是如何设计与实现的?Facebook 用户可访问主页:UltraincommunityLink:http://fb.me/Ultraincommunity

区块链技术咖口中的世界状态,究竟是如何设计与实现的?Twitter 用户可关注:@UltrainBLink:https://twitter.com/UltrainB

区块链技术咖口中的世界状态,究竟是如何设计与实现的?Telegram 用户可加入电报群:Link:https://t.me/UltrainOfficial区块链技术咖口中的世界状态,究竟是如何设计与实现的?

-END-

来源链接:mp.weixin.qq.com