Skip to main content
已提交状态必须满足两项相互拉扯的需求。执行需要快速拿到某个键的当前值,每秒数百万次。验证需要一份证明,说明在某个特定版本上该值确实就是网络所声称的那个。用一种结构同时服务两者,只会两件事都做不好。 所以存储把问题拆到几个彼此独立的存储里,每一个都按自己的访问模式来设计。

三个存储

已提交区块
账本存储交易 · 输出 · 事件 · 写集 · 累加器 · 区块元数据
重放与审计
状态 KV 存储当前值与历史值,分成十六个分片,高频状态另置一层
查询与执行
状态 Merkle 存储一棵带版本的稀疏 Merkle 树,外加跟踪被取代节点的索引
验证 — 证明
只需要取值的读取,不必为树遍历买单;而一份证明,也不必从一个为点查询优化的存储里硬凑出来。
账本存储保存链的历史:交易,交易的输出与辅助数据,事件,写集,区块元数据,以及累加器。这是你用来重放的东西。 状态 KV 存储保存状态值,按键和版本编址。这是执行与查询读取的东西。它被分成十六个分片,因此一个区块的写入会散布到十六个相互独立的 RocksDB 实例上,而不是争抢同一个。高频访问的状态另外放在自己的一层里,活跃市场的工作集就不必到链上有史以来存过的所有东西里去翻。 状态 Merkle 存储保存认证结构——用来证明某个值的树节点,以及跟踪哪些节点已被取代的索引。 拆开的好处是:只需要取值的读取,不必为树遍历买单;而一份证明,也不必从一个为点查询优化的存储里硬凑出来。

认证

两种 Merkle 结构做的是不同的工作。
带版本的稀疏 Merkle 树
累加器
你的键及其值
兄弟节点哈希
兄弟节点哈希
状态根,由共识提交
你的交易,或一个事件
交易累加器 · 事件累加器各自承诺到目前为止按顺序包含的全部内容
账本根,由共识提交
树证明某个值曾是什么;累加器证明发生了什么、以何种顺序发生。两者合起来,不必持有整条链,也能证明“这笔交易在链上的这个位置”。
一棵带版本的稀疏 Merkle 树为状态提供认证。每个键的位置由其哈希决定,而树的每个版本都共享那些没有变化的节点——所以在一个区块里写入一个键,增加的是一条路径,而不是一棵树。某个键在某个版本上的证明,就是一条路径:从这个键的叶子,走到网络提交的那个根。 累加器为顺序提供认证。一个累加交易,一个累加事件,各自产生一个根,承诺了到目前为止按顺序包含的全部内容。正因如此,不必持有整条链,也能证明“这笔交易在链上的这个位置”。 两者之间的分工:状态树证明某个值曾是什么,累加器证明发生了什么以及以何种顺序发生。

推测状态

区块的结果在提交之前就已经存在。与其先写进持久化的树、区块万一没提交再撤回,不如把未提交状态放在最近一个已提交版本之上的一层内存稀疏 Merkle 覆盖层里。 执行透过这层覆盖读取,看到的是一致的视图。区块提交了,覆盖层就物化下去;没提交,覆盖层直接丢掉,持久化存储自始至终没被碰过。正是这一点让推测执行不会在存储里留下残渣。

缓存

树节点在两个层次上缓存:一个感知版本的缓存,保持近期版本可寻址;其下是一个最近最少使用(LRU)缓存。一条交易链的访问模式——一小组每个区块都会触及的热点键,加上一条极少被触及的长尾——正是这类缓存所针对的形态。

裁剪

每一个版本都永久留着,是选择,不是要求。三个相互独立的裁剪器分别针对这三个存储运行,各有自己的保留策略:一个针对账本,一个针对状态值,一个针对 Merkle 节点。 Merkle 裁剪器和状态值裁剪器由与数据同时写入的过期索引驱动。某个版本取代了一个节点或一个值,被取代的那一条就在这个版本上记为过期。于是裁剪变成对一个索引做区间扫描,而不是去搜寻垃圾——写入方早已说明了什么会变得可回收,以及在什么时候。
保留策略是运维决定,后果实打实。裁剪激进的节点,当前状态照样提供得很快,历史查询却答不了,也没法给一个从更早处起步的节点做状态同步。归档节点保留一切,也为此付钱。参见运行节点。

备份与恢复

这些存储可以脱开运行中的节点单独备份、单独恢复。正因如此,才能从快照拉起一个节点,不必从创世重放;也才能对着已提交的根去验证恢复出来的状态,而不是选择相信那份备份。

后续阅读

状态同步

节点如何在不重放全部历史的情况下追上这条链。

状态模型

存储的是什么,以及哪一种表示是权威。

索引器

从已提交记录中重建历史。

运行节点

节点角色,以及如何咨询节点运营事宜。