对 EIP-4844 proto-Danksharding 的全方位释疑
来源 | notes.ethereum.org
fake_exponential
的工作机制是什么样的?Danksharding 是以太坊的新分片设计提案,与以前的设计相比,这个提案引入了一些重要的简化。
自 2020 年以来提出的所有最新的以太坊分片提案 (Danksharding 和 Danksharding 之前的),与大多数非以太坊分片提案的主要区别在于以太坊的以 rollup 为中心的路线图 (请参见:[1] [2] [3]):以太坊的分片方案是给 data blob 提供更多的空间而不是交易,且以太坊协议本身并不试图解译这些 data blob。验证一个 blob 只需要检查该 blob 是否可用——即它能否从网络上下载。这些 blob 的数据空间预期是为支持高吞吐量交易的二层 rollup 协议所用的。
Danksharding (请参见: [1] [2] [3]) 引入的主要创新是费用市场被统合起来 了:在 Danksharding 的设计里,没有固定数量的分片,且每个分片都有不同的区块和不同的区块提议者,而是只有一个提议者选择所有交易,且所有数据都进入该 slot。
为了避免这个设计对验证者有过高的系统要求,我们引入提议者/构建者分离方案 (PBS) (请参阅:[1] [2]):一类被称为区块构建者 (block builder) 的专业化行动者会通过竞标的方式争取对 slot 内容的选择权,而提议者只需要选择出价最高的有效区块头。只有区块构建者需要处理整个区块 (即使在这里,也可以使用第三方去中心化预言机协议来实现分布式的区块构建者);所有其他的验证者和用户都可以通过数据可用性采样来有效验证区块 (请记住:区块的“大”部分都只是数据)。
proto-danksharding (即 EIP-4844) 是用来实现完整 Danksharding 规范大多数的逻辑和“支架” (即交易格式和验证规则) 的一个提案,但还没实际上实现任何分片。在 proto-danksharding 实现里,所有验证者和用户仍然需要直接对完整数据的可用性进行直接验证。
proto-danksharding 引入的主要功能是一种新的交易类型,我们把它称为携有 blob 的交易 (blob-carrying transaction) 。一笔携有 blob 的交易就像一笔普通交易,除了它还携有一节额外的被称为 blob 的数据。Blob 是非常大的 (~125 kB),且比数量相当的 calldata 便宜很多。但是,blob 数据是 EVM 执行不能访问的;EVM 只能查看对 blob 的承诺。
因为验证者和客户端仍然不得不下载完整的 blob 内容,在 proto-danksharding 里,数据带宽是以每个 slot 1 MB 而不是完整的 16 MB 为目标的。然而,这仍然能大大提升可扩展性,因为这些数据并不会与现有的以太坊交易竞争 gas 的使用。
这与平均负载和最坏情况负载之间的区别相关。今天的情况是我们的平均区块大小已经达到大约 90 kB,而理论可能的最大值 (如果区块的所有 30M gas 都用于 calldata) 是大约 1.8 MB。以太坊网络在过去曾处理过接近最大容量的区块。然而,如果我们只是把 calldata 的 gas 开销降低 10 倍,这样尽管平均区块大小会膨胀到仍然可以接受的水平,但最坏情况可以变成 18 MB,这远非以太坊网络可以负荷的。
目前的 gas 定价方案决定了平均负载和最坏情况负载是不可能分开的:平均负载和最坏情况负载的比率是由用户选择在 calldata 和其他资源上花费多少 gas 决定的,这意味着 gas 价格必须基于最坏情况的可能性来设置,这也导致平均负载不必要地低于系统可以负荷的水平。但如果我们改变 gas 定价方案,明确创建一个多维度的费用市场,我们可以避免平均情况/最坏情况负载之间的不匹配 ,并在每个区块包含接近我们可以安全处理的最大数据量。Proto-danksharding 和 EIP-4488这两个提案做的正是这样的事。
平均情况下的区块大小 | 最坏情况下的区块大小 | |
---|---|---|
现状 | 85 kB | 1.8 MB |
EIP-4488 | 未知;如果 calldata 使用增长 5 倍,是 350 kB | 1.4 MB |
Proto-danksharding | 1 MB (如果想的话可以调整) | 2 MB |
EIP-4488 是一个更早期和更简单的尝试,以解决同样的平均情况/最坏情况负载不匹配问题。EIP-4488 通过两个简单规则做到了这一点:
硬性上限是确保平均情况的更大幅增长不会导致最坏情况负载增长的最简单可用方法。gas 开销的减少会大幅增加 rollup 的使用,很可能使平均区块大小增长到数百 Kb,但单个区块包含 10 MB 这样的最坏情况可以直接通过硬性上限来避免。事实上,最坏情况的区块大小将比现在还低 (1.4 MB vs 1.8 MB)。
Proto-danksharding 则创建了一个单独的交易类型,它可以在大型固定大小的 blob 里存储更便宜的数据,但每个区块可以包含多少个 blob 是有限制的。这些 blob 不能从 EVM 访问 (只有对这些 blob 的承诺可以),且这些 blob 是存储在共识层 (信标链) 而不是执行层的。
EIP-4488 与 proto-danksharding 的主要实际区别在于,EIP-4488 试图将今天以太坊所需做出的变更降到最低,而 proto-danksharding 则对今天以太坊做出更多的变更,以便在未来升级到完整版分片时只需做出很少的变更。虽然实现完整分片 (有数据可用性采样等) 是一项很复杂的任务,而且实现 proto-danksharding 后还有很复杂的工作,这些复杂性都会控制在共识层上。一旦 proto-danksharding 部署了,执行层客户端团队、rollup 开发者和用户在过渡到完整分片时都不需要做任何额外的工作了。
请注意,现在不是二选一:我们会很快实现 EIP-4488,然后在半年后实现 proto-danksharding。
引用 EIP-4844:
在这个 EIP 里已经实现的工作包括:
- 一种新的交易类型,与完整分片里需要存在的交易格式完全相同
- 完整分片里所需的全部执行层逻辑
- 完整分片里所需的全部执行/共识交叉验证逻辑
BeaconBlock
验证和数据可用性 blob 采用间的层分离- 完整分片里所需的大部分
BeaconBlock
逻辑- 用于 blob 的、自我调整的独立 gas 定价
要实现完整版分片还需要完成的工作包括:
- 在共识层上
blob_kzgs
的低次扩展 (low-degree extension) 使得 2D 采样变得可能- 数据可用性采样的实际实现
- PBS (提议者/构建者分离方案),以避免要求个人验证者在一个 slot 里处理 32 MB 的数据
- 委托证明 (Proof of Custody) 或类似的在每个区块里每个验证者都要验证一部分的分片数据的协议内要求
请注意,所有这些剩下的工作是发生在共识层,并不要求执行客户端团队、用户或rollup 开发者有额外的工作。
EIP-4488 和 proto-sharding 都会导致需要长期最大限度地使用每 slot (12 秒) 大约 1 MB 的容量。这相当于每年 2.5 TB,远高于以太坊今天需要的增长率。
在 EIP-4488 里,解决这个问题需要历史数据过期机制 (EIP-4444) ,这样客户端就不再需要存储超过某个时间段 (期限从一个月到 1 年都有提议) 的历史数据。
在 proto-danksharding 里,无论 EIP-4444 是否得到实现,共识层都可以实现在某个时间 (比如 30 天后) 后自动删除 blob 数据的独立逻辑 。但是,无论采用什么短期的数据扩容方案,我都强烈建议尽快实现 EIP-4444。
这两种策略都将共识客户端的额外磁盘负载限制在最多几百 GB。从长期来看,采用某种历史数据过期机制基本上是强制性的 :完整分片会每年增加大约 40 TB 的历史 blob 数据,因此用户实际上只能在一段时间内存储其中的一小部分。因此,尽早设好这个问题的预期是值得做的。
以太坊共识层的目的不是保证永远保存所有历史数据。相反,其目的是提供一个高度安全的实时公告板,并为其他去中心化协议留有做长期存储的空间 。公告板的存在是为了确保在公告板上发布的这些数据在足够长的时间里是可用的,使得需要这些数据的用户,或任何想要备份这些数据的长期协议有充足的时间抓取数据,并把它导入他们的其他应用或协议中。
一般来说,长期历史数据的存储是容易的。尽管 2.5 TB 每年对普通节点来说要求太高了,但对于专业用户来说是非常容易管理的:你可以购买非常大的硬盘,每 TB 大约 20 美元,对于爱好者来说是承担得起的。不同于共识的信任模型是 N/2-of-N,历史存储的信任模型是 1-of-N:你只需要一个数据存储者是诚实的。因此,每个历史数据段只需要被存储数百次,而不是在实时共识验证的数千个节点全集。
完整的历史数据被完整存储并能易于访问的实际方法包括:
当历史数据存储要求到达比较高的水平 (例如 500 TB 每年),一些数据被遗忘的风险会变高(此外,数据可用性验证系统变得更加紧张)。这可能是分片区块链可扩展性的真正极限。但是,目前提出的所有参数距离这个点还很远。
blob 是一个矢量为 4096 的字段元素,数字都在这个范围:
0 <= x < 52435875175126190479447740508185965837690552500527637822603658699938581184513
blob 在数学上是在以上模数有限域多项式中< 4096 的次项,其中在 blob 里处于位置 i 的字段元素就是多项式在 $w^{i}$ 位置上的取值。w 是满足 $w^{4096}=1$ 的常数。
对一个 blob 的承诺就是对多项式的 KZG 承诺的一个哈希。但是,从实现的角度来看,对多项式数学细节的关心并不那么重要。相反,将只会有一个椭圆曲线点的矢量 (以拉格朗日插值法为基础的受信任初始化 ),对 blob 的 KZG 承诺将会只是一个线性组合。引用 EIP-4844 的代码:
<pre><div><code class="language-Powershell"><span>def blob_to_kzg(blob: Vector[BLSFieldElement, 4096]) -> KZGCommitment: </span> computed_kzg = bls.Z1 for value, point_kzg in zip(tx.blob, KZG_SETUP_LAGRANGE): assert value < BLS_MODULUS computed_kzg = bls.add( computed_kzg, bls.multiply(point_kzg, value) ) return computed_kzg</code></div></pre>
BLS_MODULUS
即上述模数,而 KZG_SETUP_LAGRANGE
是椭圆曲线上点的矢量,即以拉格朗日插值法为基础的受信任初始化 (trusted setup)。对于实现者,现在只需要把它看作一个黑盒子般的用于特殊目的的散列函数。
EIP-4844 并没有直接使用 KZG 来表示 blob,而是使用版本化的哈希 (versioned hash) :一个 0x01 字节 (代表版本) 后面跟着 KZG 的 SHA256 哈希的后 31 字节。
这样做是为 EVM 兼容性和未来的兼容性:KZG 承诺的大小是 48 字节,但 EVM 更习惯于处理 32 字节的值,而且如果我们从 KZG 转换到其他东西 (例如,出于抗量子计算的原因),承诺还可以继续是 32 字节的。
Proto-danksharding 引入两个预编译:blob 验证预编译 (blob verification precompile )和点取值预编译 (point evaluation precompile )。
blob 验证预编译 很好理解:它把一个版本化的哈希和一个 blob 作为输入,然后验证提供的版本化哈希是否真的是该 blob 的有效版本化哈希。这个预编译旨在被 optimistic rollup 使用。引用 EIP-4844:
Optimistic rollup 只有在提交欺诈证明时才真的需要提供基础数据。欺诈证明的提交函数会要求将有欺诈性的 blob 的全部内容作为 calldata 的一部分来提交。它会依据之前提交的版本化哈希用 blob 验证函数来对数据进行验证,然后就像现在一样对该数据进行欺诈证明验证。
点取值预编译 把一个版本化的哈希、一个x
坐标、一个 y
坐标,和一个证明 ( blob 的 KZG 承诺和一个 KZG 取值证明) 作为输入。它对证明进行验证,以检查 P(x) = y
,其中 p
是一个多项式,它由已经有版本化哈希的 blob 来表示。这个预编译旨在被 ZK rollup 使用。引用 EIP-4844:
ZK rollup 会提供对它们的交易或状态变换数据提供两个承诺:blob 里的 kzg 和使用 ZK rollup 内部使用的任何证明系统的某种承诺。它们会使用一个承诺证明等效性协议,通过使用点取值预编译来证明 kzg 承诺 (该协议确保它指向可用数据) 与 ZK rollup 自己的承诺是值相同的数据。
请注意,大多数主要的 optimistic rollup 设计使用的是一种多轮的欺诈证明方案,其中最后一轮只使用少量的数据。因此,可以想象, optimistic rollup 也可以使用点取值预编译 而不使用 blob 验证预编译,而且这样还更便宜。
在一个 ZK rollup 里用“天真”的方法对 blob 进行检查就是把 blob 数据作为 KZG 的私人输入,并在 SNARK 中进行椭圆曲线的线性组合 (或配对) 来对它验证。这是错的,且带来不必要的低效。其实,有一个简单得多的方法 。
设 K 为 KZG 承诺,B 为所承诺的 blob。所有的 ZK-SNARK 协议都有某种方法可以将大量数据导入证明中,并包含某种对该数据的承诺。例如,在 PLONK 里,就是 $Q_{C}$ 承诺。
我们要做的就是证明 K 和 Q_{C} 是相同数据的承诺。这可以通过一个证明等效协议来实现,这非常简单。从该文复制过来:
假设你有多个多项式承诺 $C{1}...C{k}$,在 k 个不同的承诺方案 (例如 Kate、FRI、某个基于 bulletproof 的方案、DARK 等) 中,你想证明它们是对同一个多项式 P 承诺的。我们可以轻易证明这点:
让 $z=hash (C{1}....C{k})$,我们把 z 解析为一个 P 有取值的取值点。
公布“打开”$O{1}...O{k}$,其中$O{i}$ 是这样一个证明:在第 i 个承诺方案下,$C_{i}(z)=a$。然后验证在所有的方案中 a 是同一个数字。
一笔 ZK rollup 交易只需要有一个普通的 SNARK 和一种与之等效的证明,以证明它的公开数据与版本化的哈希相等。请注意,它们不应该直接执行 KZG 检查;而是应该只使用点取值预编译 来验证这个“打开”。这确保了这个方案不会过时:如果以后 KZG 被其他东西取代了,ZK rollup 也能继续运作,而不会有其他问题。
参阅:
在以太坊里,现在的计划是并行运行 4 个仪式 (分别有不同的秘密),它们的大小是 $(n{1} = 4096$, $n{2} = 16$), ($n{1} = 8192$, $n{2} = 16$) , ($n{1} = 16384$, $n{2} = 16$) 和 ($n{1} = 32768$, $n{2} = 16$) 。理论上只需要第一个,但用更大的大小来运行能提高未来可用性,因为能允许我们增大 blob 的大小。我们不能只是有一个更大型的初始化,因为我们想要能够在多项式的次数上有一个硬性上限,使得多项式可以进行有效承诺,这个上限等于 blob 的大小。
实现这点的可能切实有效方法是以 Filecoin 的初始化开始,然后运行一个扩展它的仪式。多种实现,包括浏览器的实现,能使更多人参与其中。
不幸的是,使用 KZG 以外的任何东西 (例如 IPA 或 SHA256) 都会使分片路线图更加困难。有以下几个原因:
因此,很不幸地,使用 KZG 以外的东西带来的功能损失和复杂性增加都比 KZG 本身带来的风险要大得多。此外,任何 KZG 相关的风险都是有限的:一次 KZG 故障只会影响依赖 blob 数据的 rollup 和其他应用,而不会影响系统的其他部分。
KZG 承诺于 2010 年的一篇论文被提出,并在从那时起到 2019 年被广泛应用在 PLONK 式的 ZK-SNARK 协议中。然而,KZG 承诺底层的数学是相对简单的算术,建基于椭圆曲线运算和配对。
具体使用的曲线是 BLS12-381,它诞生于在 2002 年被发明的 Barreto-Lynn-Scott 族。椭圆曲线配对是 KZG 承诺所必需的,是非常复杂的数学,但它们在 20 世纪 40 年代就已经被发明,并从 90 年代开始应用于密码学。到了 2001 年,有非常多提出来的加密算法是使用配对的。
从实现的复杂性来看,KZG 的实现并不会比 IPA 难很多:用于计算承诺的函数 (见上文“blob 数据是什么格式,它是如何被承诺的?”) 与在 IPA 里使用的完全相同,只是用了一组不同的椭圆曲线点常数。而点验证预编译是更复杂的,因为它涉及配对取值,但用到的数学与已经在 EIP-2537 (BLS12-381 预编译)实现的部分是相同的,且与 bn128 配对预编译 (也请看: 优化的 python 实现) 非常相似。因此,不需要复杂的“新工作”来实现 KZG 验证。
有四个主要组件:
BeaconBlockBody
里的 blob KZG 承诺列表BeaconBlock
传送BlobTransactionNetworkWrapper
(请看这个 EIP 的网络部分)ExecutionPayload
,KZG 放入信标区块以及主体放入侧车请注意,对于一个最小实现,我们根本不需要交易池 (我们可以依靠第二层交易打包市场),我们只需要一个客户端来实现区块构建逻辑。只有执行层和共识层的共识变更需要广泛的共识测试,但它们是相对轻量级的。在最小实现和推出“完整”产品 (所有客户端都支持出块和交易池) 都是可能的。
proto-danksharding 引入了多维度 EIP-1559 费用市场,其中有两种资源—— gas 和 blob,二者的浮动 gas 价格和上限是独立的 。
也就是说,有两个变量和四个常数:
每个区块的目标 | 每个区块的上限 | 基本费用 | |
---|---|---|---|
Gas | 1500万 | 3000万 | 可变的 |
Blob | 8 | 16 | 可变的 |
blob 费用是以 gas 来收取的,但它的 gas 量是可变的,它的实时调整使得从长远来看,每个区块的平均 blob 数实际上会等于目标值。
二维的特性意味着区块构建者将面临一个更难的问题:不同于简单地通过按最高的优先费用来接受交易,直到没有交易了或达到区块 gas 上限,他们必须同时避免触达两个不同的上限。
举个例子 。假设 gas 上限是 70,blob 上限是 40。交易池里有很多交易,足够填充区块,有两种类型的交易 (交易 gas 包含每 blob 的 gas):
一个矿工如果遵循天真的“选择优先费用最高”的算法,他会用 10 笔 (40 gas) 第一种类型交易来填充整个区块,获得收益 5 40 = 200。因为这 10 笔交易已经完全用完了 blob 的数量上限,它们就不能打包其他交易了。但是,最优的策略是打包 3 笔第一种类型和 28 笔第二种类型的交易。这样的区块有 10 个 blob,消耗 68 gas,带来的收益是 5 12 + 3 * 56 = 228。
执行客户端现在需要执行复杂的多维度背包问题算法来优化它们的出块吗?不需要,有以下几个原因:
考虑到这些原因,更复杂的费用市场动态并不会大大增加中心化或带来风险;事实上,这个原则应用地越广,实际上可以降低 DoS 风险!
今天的 EIP-1559 调整基本费用 b,以实现一个特定的目标 gas 使用水平 t,如下:
$b{n+1}=b{n}*(1+\frac{u-t}{8t})$
其中,$b{n}$ 是当前的区块基本费用,$b{n+1}$ 是下一个区块的基本费用,t 是目标,u 是已使用的 gas。
这个机制的一个问题是,它实际上并不以 t 为目标。假设我们有两个区块,第一个 u=0,下一个 u=2t。那我们会得到:
区块高度 | 基本费用 |
---|---|
n | x |
x+1 | $\frac{7}{8}∗x$ |
x+2 | $\frac{7}{8}∗\frac{9}{8}∗x=\frac{63}{64}∗x$ |
尽管平均使用量为 t,基本费用是按系数$\frac{63}{64}$下降的。因此,基本费用只有当使用量略高于 t 的时候才会稳定下来;实际情况是明显高出 3%,尽管这取决于方差。
更好的公式是指数式调整:
$b_{n+1}=b_n∗exp(\frac{u−t}{8t}) $
exp(x) 是指数函数$e^{x}$,其中e≈2.71828。当 x 的数值比较小时,$exp(x)≈1+x$。但是,它有个很实用的属性,就是交易替换不互相影响:多步调整
$b^{n}exp(\frac{u_{1}-t}{8t})$ ... * $exp(\frac{u_{n}-t}{8t})$
只取决于 u{1}+...+ u{n} 的总和,而不是分布。原因是,我们可以做一下计算:
$b{n}*exp(\frac{u{1}-t}{8t})$ ... $exp(\frac{u_{n}-t}{8t})$
$=b{n}*exp(\frac{u{1}-t}{8t}+...+\frac{u_{n}-t}{8t})$
$=b{n}*exp(\frac{u{1}+...+u{n}-u{t}}{8t})$
因此,打包相同的交易最终的基本费用是相同的,无论他们在不同的区块里是如何分布的。
上文最后一个公式也有一个合理的数学解释:$(u{1}+...+u{n}-u{t})$ 可以被看作是超出的部分 :实际消耗的总 gas 与计划要消耗的总 gas 之间的差值。当前的基本费用等于 $b{0}*exp(\frac{excess}{8t})$这一点非常清楚地表明超出的部分不能超出一个非常狭小的范围:如果它超过了 8t∗60,那么基本费用就会变成 $e^{60}$,这就变成高得夸张,以至于没有人会支付,如果它变成低于 0,那么这项资源基本上就变成是免费的,这条链就会被垃圾内容填充直到超出部分升回 0 以上。
这个调整机制的工作原理是这样的:它跟踪 actual_total
$(u{1}+...+u{n})$ 并计算targeted_total
(nt),还把差值的指数作为价格来计算。为了让计算变得简单,我们使用$ 2^{x}$ 而不用$e^{x}$;事实上,我们使用一个$ 2^{x}$的近似值 :EIP 里的 fake_exponential
(假指数) 函数。这个假指数几乎一直维持在实际数值的 0.3% 以内。
为了防止长时间使用量过低导致之后的长时间两倍容量满块,我们增加一个额外的功能:我们不让超出部分 低于 0。如果出现 actual_total
低于 targeted_total
,我们就把 actual_total
设为等于 targeted_total
。这样做的确会在极端情况里 (blob 的 gas 会一直下降至 0) 破坏交易顺序的不变性,但为了增加安全性,这是一个可以接受的权衡。
请注意,这个多维度市场还会出现一个有趣的现象:当 proto-danksharding 第一次引入时,可能一开始会没什么用户,因此在一段时间里,一个 blob 的开销几乎肯定会非常便宜,即使“普通”以太坊区块链活动还是很昂贵 。
笔者认为,这个费用调整机制比现在的好,且最终 EIP-1559 费用市场里的所有资源都会转为有自己的 EIP-1559 机制。
更长更详细的解释,请看 Dankrad 的文章。
fake_exponential
的工作机制是什么样的?为了方便起见,这是 fake_exponential
的代码:
def fake_exponential(numerator: int, denominator: int) -> int:
cofactor = 2 ** (numerator // denominator)
fractional = numerator % denominator
return cofactor + (
fractional * cofactor * 2 +
(fractional ** 2 * cofactor) // denominator
) // (denominator * 3)
这是用数学重新表达的核心机制,去掉了舍入:
$FakeExp(x)=2^{⌊x⌋} ∗Q(x−⌊x⌋) $
$Q(x)=1+\frac{2}{3}x+\frac{1}{3}x^{2}$
目标是把很多的 Q(x) 实例拼凑起来,x 的一个位移,就会按比例缩放大概一个$ [2^{k},2^{k+1}]$ 的值域。Q(x) 本身是$ 2^{x} $ 的近似值,其中 0≤x≤1,因以下属性被选择:
后三个要求给出三个未知系数的三个线性方程,而以上的所有属性使得 Q(x) 只能得出一个解。
这个逼近方式的效果出乎意料地好;除了最小值的输入,fake_exponential
给出的答案都在$ 2^{x}$实际数值的 0.3% 以内:
fake_exponential (蓝线) vs $ 2^{x} $ 的实际数值,其中 0≤x≤5, 步长为 20。 |
fake_exponential 被 $ 2^{x} $的实际数值除, 其中 5≤x≤50, 步长为 20。 |
注意:这部分很容易过时。不要相信它能对任何特定问题提供最新的想法。
ExecutionPayload
改为一个联合类型?(这是一个“现在做更多的工作” vs “以后做更多工作”的权衡)ECN的翻译工作旨在为中国以太坊社区传递优质资讯和学习资源,文章版权归原作者所有,转载须注明原文出处以及ethereum.cn,若需长期转载,请联系eth@ecn.co进行授权。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!