EIP-7495: SSZ 稳定容器
一种新的 SSZ 类型,用于表示具有稳定序列化和梅克尔化的灵活容器
Authors | Etan Kissling (@etan-status), Cayman (@wemeetagain) |
---|---|
Created | 2023-08-18 |
Discussion Link | https://ethereum-magicians.org/t/eip-7495-ssz-stablecontainer/15476 |
Requires | EIP-7916 |
Table of Contents
摘要
此 EIP 引入了两种新的 简单序列化 (SSZ) 类型,以实现向前兼容的容器。
StableContainer[N]
扩展了 SSZ Container
,即使在将来弃用单个字段或引入新字段时,也能实现稳定的梅克尔化和向前兼容的序列化。
此外,引入 Profile[B]
以支持 StableContainer[N]
的专用子类型,同时保留基本类型的梅克尔化。这很有用,例如,对于仅使用部分基本字段和/或需要已知基本字段的特定于分叉的数据结构。这些数据结构的 Merkle 证明的验证者不会在新分叉上中断。
动机
SSZ 目前无法表示稳定的容器和 Profile。添加支持具有以下好处:
-
稳定的签名: 从
StableContainer[N]
派生的签名根永远不会改变。在以太坊的上下文中,这对于预期即使在未来的更新中引入了额外的交易字段仍然有效的交易签名非常有用。同样,整体交易根保持稳定,可以用作永久交易 ID。 -
稳定的梅克尔证明: 检查
StableContainer[N]
或Profile[B]
的特定字段的 Merkle 证明验证器在未来的更新引入额外的字段时不需要持续更新。公共字段始终在相同的通用索引处进行梅克尔化。 -
可选字段: 当前的 SSZ 格式不支持可选字段,从而促使设计使用零值代替。使用
StableContainer[N]
和Profile[B]
,SSZ 序列化是紧凑的;非活动字段不占用空间。
规范
本文档中的关键词“必须”、“禁止”、“必需”、“应”、“不应”、“应该”、“不应该”、“推荐”、“不推荐”、“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。
注意:在本文档中,Optional[T]
专门指 Python 的 typing.Optional
。具体来说,Optional[T]
本身不是 SSZ 类型!
StableContainer[N]
与常规 SSZ Container
类似,StableContainer[N]
定义了字段的有序异构集合。N
表示将来可以增长到的最大字段数。N
必须 > 0
。
StableContainer[N]
的所有字段必须是 Optional[T]
类型。此类字段可以表示 SSZ 类型 T
的当前值,或表示缺少值(由 None
指示)。 Optional[T]
的默认值为 None
。
class Example(StableContainer[32]):
a: Optional[uint64]
b: Optional[uint32]
c: Optional[uint16]
为了序列化的目的,无论各个字段类型如何,StableContainer[N]
始终被认为是“可变大小”的。
稳定性保证
只要满足以下条件,StableContainer[N]
的序列化和梅克尔化将保持稳定:
- 最大容量
N
不变 - 字段的顺序不变
- 新字段始终附加到末尾
- 所有字段都具有不可变的 SSZ 模式,或者递归采用
StableContainer[N]
List
/Bitlist
容量不变;请考虑使用ProgressiveList
代替,或者通过应用程序逻辑缩短
JSON 序列化
JSON 序列化遵循 SSZ Container
的规范 JSON 映射。
值为 None
的类型 Optional[T]
的字段在序列化为 JSON 时应省略。
二进制序列化
类似于 Container
的现有逻辑定义了 StableContainer[N]
的序列化。值得注意的变化是:
- 构造一个
Bitvector[N]
,指示StableContainer[N]
中的活动字段。对于具有当前值的字段(非None
),包含一个True
位。对于具有None
值的字段,包含一个False
位。Bitvector[N]
填充有False
位,直到长度N
- 仅序列化活动字段,即在
Bitvector[N]
中具有相应True
位的字段 Bitvector[N]
的序列化附加到序列化的活动字段的前面- 如果序列化了可变长度字段,则它们的偏移量相对于序列化的活动字段的开头,即在
Bitvector[N]
之后
# 确定活动字段
active_fields = Bitvector[N](([element is not None for element in value] + [False] * N)[:N])
active_values = [element for element in value if element is not None]
# 递归序列化
fixed_parts = [serialize(element) if not is_variable_size(element) else None for element in active_values]
variable_parts = [serialize(element) if is_variable_size(element) else b"" for element in active_values]
# 计算并检查长度
fixed_lengths = [len(part) if part != None else BYTES_PER_LENGTH_OFFSET for part in fixed_parts]
variable_lengths = [len(part) for part in variable_parts]
assert sum(fixed_lengths + variable_lengths) < 2**(BYTES_PER_LENGTH_OFFSET * BITS_PER_BYTE)
# 将可变大小部分的偏移量与固定大小部分交错
variable_offsets = [serialize(uint32(sum(fixed_lengths + variable_lengths[:i]))) for i in range(len(active_values))]
fixed_parts = [part if part != None else variable_offsets[i] for i, part in enumerate(fixed_parts)]
# 返回活动字段 `Bitvector` 与活动
# 固定大小部分(偏移量交错)和活动可变大小部分的串联
return serialize(active_fields) + b"".join(fixed_parts + variable_parts)
反序列化
StableContainer[N]
的反序列化首先反序列化 Bitvector[N]
。必须验证该值:
Bitvector[N]
中超过字段数的额外位必须为False
其余数据与常规 SSZ Container
相同地反序列化,查阅 Bitvector[N]
以确定哪些字段存在于数据中。在反序列化期间跳过缺少的字段,并分配 None
值。
梅克尔化
通过以下帮助函数扩展了梅克尔化规范:
chunk_count(type)
:计算类型梅克尔化的叶子数量。StableContainer[N]
:始终为N
,无论类型定义中实际字段的数量如何
mix_in_aux
:给定一个 Merkle 根root
和一个辅助 SSZ 对象根aux
,返回hash(root + aux)
。
要梅克尔化 StableContainer[N]
,将构造一个 Bitvector[N]
,指示 StableContainer[N]
内的活动字段,使用与序列化期间相同的过程。
对象 value
的梅克尔化 hash_tree_root(value)
扩展为:
mix_in_aux(merkleize(([hash_tree_root(element) if element is not None else Bytes32() for element in value.data] + [Bytes32()] * N)[:N]), hash_tree_root(value.active_fields))
如果value
是StableContainer[N]
。
Profile[B]
Profile[B]
还定义了一个有序的异构字段集合,它是具有以下约束的基本 StableContainer
类型 B
的字段的子集:
Profile[B]
中的字段对应于B
中具有相同字段名称的字段。Profile[B]
中的字段遵循与B
中相同的顺序。- 基本
StableContainer
类型B
中的字段均为Optional
。- 可以通过省略
Profile[B]
中的字段来禁止使用它们。 - 可以通过将它们保留为
Optional
来使这些字段在Profile[B]
中保持可选。 - 可以通过从
Optional
中解包来使这些字段在Profile[B]
中成为必需。
- 可以通过省略
Profile[B]
中的所有字段类型必须与B
中相应的字段类型兼容。- 字段类型与自身兼容。
byte
与uint8
兼容,反之亦然。- 如果
Bitlist[N]
/Bitvector[N]
字段类型共享相同的容量N
,则它们是兼容的。 - 如果
T
兼容并且它们也共享相同的容量N
,则List[T, N]
/Vector[T, N]
字段类型是兼容的。 - 如果
T
兼容,则ProgressiveList[T]
字段类型是兼容的。 - 如果所有内部字段类型兼容,如果它们也共享相同顺序的相同字段名称,并且对于
StableContainer[N]
,如果它们也共享相同的容量N
,则Container
/StableContainer[N]
字段类型是兼容的。 Profile[X]
字段类型与与X
兼容的StableContainer
类型兼容,并且与Profile[Y]
兼容,其中如果所有内部字段类型也兼容,则Y
与X
兼容。仅在可选性方面的差异不会影响梅克尔化兼容性。
序列化
Profile[B]
的序列化与其基本 StableContainer[N]
的序列化相似,不同之处在于前导 Bitvector
被稀疏表示代替,该稀疏表示仅包括有关 Profile[B]
中可选字段的信息。不包括 Profile[B]
的必需字段的位以及到容量 N
的零填充。如果 Profile[B]
中没有可选字段,则省略 Bitvector
。
当且仅当 Profile[B]
包含任何 Optional[T]
或任何“可变大小”字段时,才认为 Profile[B]
是“可变大小”的。
梅克尔化
Profile[B]
的梅克尔化遵循基本类型 B
的梅克尔化。
# 定义了通用的梅克尔化格式和可移植的序列化格式
class Shape(StableContainer[4]):
side: Optional[uint16]
color: Optional[uint8]
radius: Optional[uint16]
# 从 `Shape` 继承梅克尔化格式,但序列化更紧凑
class Square(Profile[Shape]):
side: uint16
color: uint8
# 从 `Shape` 继承梅克尔化格式,但序列化更紧凑
class Circle(Profile[Shape]):
color: uint8
radius: uint16
序列化示例:
-
03420001
Shape(side=0x42, color=1, radius=None)
-
420001
Square(side=0x42, color=1)
-
06014200
Shape(side=None, color=1, radius=0x42)
-
014200
Circle(radius=0x42, color=1)
虽然 Profile[B]
的这种序列化更紧凑,但请注意,它不是向前兼容的,并且必须在带外指示确定底层数据类型的上下文信息。如果需要向前兼容性,则 Profile[B]
应转换为其基本类型 B
,然后根据 B
进行序列化。
理由
StableContainer[N]
解决了哪些问题?
当前的 SSZ 类型仅在一个规范版本(即以太坊的一个分叉)中是稳定的。这对于与特定分叉相关的消息(例如,证明或信标块)是可以的。但是,对于预期在各个分叉中保持有效的消息(例如,交易或收据)来说,这是一个限制。为了支持此类永久有效消息类型的功能的演进,需要定义新的 SSZ 方案。此外,Merkle 证明的消费者可能具有与以太坊不同的软件更新节奏;实现不应仅仅因为新的分叉引入了不相关的新功能而中断。
为了避免限制设计空间,该方案必须支持扩展新字段,淘汰旧字段以及现有字段的新组合。当发生此类调整时,旧消息必须仍然可以正确反序列化,并且必须保留其原始 Merkle 根。
Profile[B]
解决了哪些问题?
即使在任何给定时间仅一个子类型有效的情况下(例如,由分叉计划确定),也可能需要 StableContainer
的向前兼容梅克尔化。在这种情况下,可以通过交换 Profile[B]
而不是底层基本类型来减小消息大小并提高类型安全性。例如,这对于诸如 BeaconState
之类的共识数据结构很有用,以确保其字段的 Merkle 证明在各个分叉中保持兼容。
为什么不是 Union[T, U, V]
?
当引入新的可选功能时,存在组合复杂性。例如,如果有三种交易类型,然后引入优先级费用作为可选功能,则必须定义三种额外的交易类型以允许包含优先级费用,从而使总交易类型增加到六种。如果随后引入了可选访问列表,则该数量再次翻倍,达到十二种交易类型。
Union
还要求网络的每个参与者都知道所有现有的 Union
情况。例如,如果执行层通过引擎 API 提供交易作为 Union
,则共识层也必须知道所有具体的 Union
情况,即使它只想打包它们并转发它们。使用 StableContainer[N]
提供了一个更类似于 JSON 的情况,其中底层类型是根据字段及其值的存在/不存在来确定的。
通常,各个 Union
情况共享某种形式的主题重叠,彼此共享某些字段。在 Union
中,共享字段不一定在相同的通用索引处进行梅克尔化。因此,即使实际更改不是特定系统感兴趣的,每次引入新风格时也必须更新 Merkle 证明系统。
为什么不将 Optional[T]
建模为 SSZ 类型?
如果将 Optional[T]
建模为 SSZ 类型,则每个单独的字段都会引入序列化和梅克尔化开销。由于 Optional[T]
将需要是“可变大小”的,因此在序列化中必须使用大量额外的偏移字节。对于梅克尔化,每个单独的 Optional[T]
都需要混合一位来指示该值的存在与否。
此外,每次字段数达到 2 的新幂时,Merkle 根将中断,因为块数增加了一倍。StableContainer[N]
通过人为地将 Merkle 树扩展到 N
个块(无论当前指定的实际字段数如何)来解决此问题。由于 N
在各个规范版本中是恒定的,因此 Merkle 树的形状保持稳定。额外的空占位符叶子的开销仅影响 Bitvector[N]
的序列化(每 8 个叶子 1 个字节);梅克尔化期间所需的哈希数仅随 N
呈对数增长。
向后兼容性
StableContainer[N]
和 Profile[B]
是新的 SSZ 类型,因此不与当前使用的其他 SSZ 类型冲突。
测试用例
请参阅 EIP 资产。
参考实现
请参阅 EIP 资产,基于 protolambda/remerkleable
。
安全注意事项
无
版权
通过 CC0 放弃版权和相关权利。
Citation
Please cite this document as:
Etan Kissling (@etan-status), Cayman (@wemeetagain), "EIP-7495: SSZ 稳定容器 [DRAFT]," Ethereum Improvement Proposals, no. 7495, August 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7495.