Vitalik:深入了解跨L2读取——跨链证明有哪些方案
在《以太坊生态系统需要三个技术转型》一文中,我概述了为什么明确考虑L1 +跨L2支持、钱包安全性和隐私作为以太坊生态系统堆栈的必要基本功能,而不是作为可以由单独的钱包单独设计的附加组件的关键原因。
本文将更直接地关注一个特定子问题的技术方面:如何使从L2读取L1、从L1读取L2或从一个L2读取另一个L2变得更容易。解决这个问题对于实施资产/密钥库(keystore)分离架构至关重要,但它在其他领域也有着有价值的用途,尤其是优化可靠的L2间调用,包括在L1和L2之间转移资产等用例。
本文目录:
目标是什么?
跨链证明是什么样的?
我们可以使用哪些证明方案?
Merkle证明
ZK SNARKs
特殊目的的KZG证明
Verkle树证明
聚合
直接状态读取
L2如何获取最新的以太坊状态根?
不是L2链的钱包
隐私保护
总结
目标是什么?
一旦L2变得更加主流,用户将在多个L2和可能的L1上拥有资产。一旦智能合约钱包(多签、社交恢复或其他类型)成为主流,访问某个账户所需的密钥将会随着时间而改变,旧的密钥将不再有效。一旦这两个情况都发生,用户将需要一种方法来更改具有权限访问位于许多不同位置的许多账户的密钥,而不需要进行大量的交易。
特别是,我们需要一种处理“虚拟地址(counterfactual addresses)”的方法:这些地址尚未以任何方式在链上“注册”,但仍需要接收和安全保管资金。我们都依赖于虚拟地址:当你第一次使用以太坊时,你可以生成一个ETH地址,他人可以用来向你支付资金,而无需在链上“注册”该地址(这将需要支付交易费用,因此需要持有一些ETH)。
对于EOAs(外部账户),所有地址最初都是虚拟地址。对于智能合约钱包而言,虚拟地址仍然是可能的,主要得益于CREATE2,它允许你拥有一个ETH地址,只有与特定哈希匹配的代码的智能合约才能填充该地址。
EIP-1014 (CREATE2) 地址计算算法
然而,智能合约钱包引入了一个新的挑战:访问密钥可能会发生变化。地址(作为initcode的哈希)只能包含钱包的初始验证密钥。当前的验证密钥将存储在钱包的存储中,但该存储记录不会自动传播到其他L2。
如果用户在许多L2上有许多地址,包括(因为它们是虚设的)所在的L2不知道的地址,那么似乎只有一种方式可以让用户更改他们的密钥:资产/密钥库分离架构。每个用户都有(i)一个“密钥库合约”(在L1或特定L2上),用于存储所有钱包的验证密钥以及更改密钥的规则,和(ii)L1和许多L2上的“钱包合约”,用于进行跨链读取以获取验证密钥。
有两种实现方法:
1、轻量级版本(仅检查以更新密钥):每个钱包在本地存储验证密钥,并包含一个函数,可以调用该函数以检查来自密钥库的当前状态的跨链证明,并更新本地存储的验证密钥。当在特定L2上首次使用钱包时,调用该函数以从密钥库获取当前验证密钥是必需的。
优点:节约使用跨链证明,因此跨链证明昂贵也没有关系。所有资金只能使用当前密钥,因此仍然安全。
缺点:要更改验证密钥,你必须在密钥库和已初始化的每个钱包(尽管不包括虚拟地址)中进行链上密钥更改。这可能需要大量的Gas费用。
2、重量级版本(每笔交易都进行检查):每个交易都需要一个跨链证明,证明密钥当前存在于密钥库中。
优点:系统复杂性较低,更新密钥库成本较低。
缺点:每笔交易都很昂贵,因此需要更多的工程来使跨链证明的成本可接受。同时,它与ERC-4337不易兼容,因为ERC-4337目前不支持在验证期间对可变对象进行跨合约读取。
跨链证明是什么样的?
为了展示全部的复杂性,我们将探讨最困难的情况:密钥库位于一个L2,钱包位于另一个L2。如果密钥库或钱包中的任何一个位于L1,则只需要这个设计的一半。
假设密钥库位于Linea,钱包位于Kakarot。完整的钱包密钥证明包括:
一个证明,根据Kakarot所知的当前以太坊状态根,证明当前Linea状态根
一个证明,根据当前Linea状态根,证明密钥库中的当前密钥
这里有两个主要的实现问题:
1、我们使用什么样的证明?(是Merkle证明吗?还是其他什么东西?)
2、L2如何首先了解最近的L1(以太坊)状态根(或者,如我们将看到的,可能是完整的L1状态)?相反,L1如何了解L2状态根?
在两种情况下,一些事情发生在一侧,直到在另一侧可以证明这件事,之间会有多长时间的延迟?
我们可以使用哪些证明方案?
有五种主要选择:
Merkle证明
通用ZK-SNARKs
特殊目的证明(例如,使用KZG)
Verkle证明,介于KZG和ZK-SNARKs之间,既涉及基础设施工作量,又涉及成本。
不使用证明,依赖于直接状态读取
就所需的基础设施工作和用户成本而言,我大致按以下顺序对它们进行排列:
“聚合”是指在每个区块内将用户提供的所有证明聚合成一个大的元证明,将所有证明组合在一起。这对于SNARKs和KZG是可能的,但对于Merkle branches不可能(你可以在一定程度上组合Merkle branches,但实际上只能节省log(每个区块的交易数)/ log(密钥库的总数)的工作量,可能实际上只有15-30%,所以成本可能不值得)。
只有在方案有大量用户时,聚合才变得有价值,因此在版本1的实施中,略过聚合,然后在版本2中实施聚合是可以的。
Merkle证明将如何工作?
这个比较简单:直接按照前一节的图表进行操作。更准确地说,每个“证明”(假设在一个L2中证明一个L2的最困难的情况)将包含:
一个Merkle branch,根据L2所知的最新以太坊状态根,证明持有密钥库的L2的状态根。持有密钥库的L2的状态根存储在已知地址的已知存储slot中(在L1上表示L2的合约),因此路径可以硬编码。
一个Merkle branch,根据持有密钥库的L2的状态根,证明当前验证密钥。在这里,验证密钥再次存储在已知地址的已知存储slot中,因此路径可以硬编码。
不幸的是,以太坊状态证明很复杂,但存在用于验证它们的库,如果使用这些库,这种机制实现不太复杂。
更大的问题是成本。Merkle证明很长,而Patricia树不幸地比必要的长度多约3.9倍(精确地说:对于包含N个对象的树,理想的Merkle证明长度为32 * log2(N)字节,而因为以太坊的Patricia树每个子节点有16个叶子,这些树的证明为32 * 15 * log16(N)~= 125 * log2(N)字节)。在大约2.5亿(~2²⁸)个帐户的状态下,每个证明的长度为125 * 28 = 3500字节,约为56,000 gas,加上解码和验证哈希的额外成本。
两个证明加在一起将花费大约100,000到150,000 gas(不包括如果每笔交易使用签名验证),比当前每笔交易的基本21,000 gas多得多。但是,如果在L2上验证证明,差距会更大。L2内的计算是廉价的,因为计算在链外完成,并且在节点数量比L1少得多的生态系统中进行。另一方面,数据必须被发布到L1。因此,不是比较21,000 gas与150,000 gas;而是21,000个L2 gas与100,000个L1 gas的比较。
我们可以通过查看L1 gas成本和L2 gas成本之间的比较来计算这意味着什么:
L1当前简单的发送操作比L2贵15-25倍,代币交换L1比L2贵20-50倍。简单的发送操作相对于数据而言比较重,但交换操作在计算方面更重。因此,交换操作是用来近似计算L1计算成本与L2计算成本的更好基准。综合考虑所有这些因素,如果我们假设L1计算成本与L2计算成本之间的成本比为30倍,这似乎意味着在L2上放置一个Merkle证明将相当于大约50笔常规交易。
当然,使用二进制Merkle树可以将成本减少约4倍,但即使如此,成本在大多数情况下仍然会太高 - 如果我们愿意不再与以太坊当前的十进制状态树兼容,我们可能还可以寻找更好的选择。
ZK-SNARK证明将如何工作?
在概念上,ZK-SNARK证明挺容易理解,只要将上图中Merkle证明替换为证明这些Merkle证明存在的ZK-SNARK。一个ZK-SNARK证明的计算成本约为400,000 gas,并占用约400字节的数据(与基本交易相比,基本交易的计算成本为21,000 gas,数据大小为100字节,在未来可以通过压缩减少到约25字节)。因此,从计算角度来看,ZK-SNARK的成本是当前基本交易成本的19倍,从数据角度来看,ZK-SNARK的成本是基本交易成本的4倍,也是未来基本交易成本的16倍。
尽管这些数字相比Merkle证明有了显著改进,但它们仍然相当昂贵。要改进这一点有两种方法:(一)特殊用途的KZG证明,或者(二)聚合,类似于ERC-4337聚合,但使用更复杂的数学。我们可以研究这两种方法。
特殊用途的KZG证明如何工作?
首先,回顾一下KZG承诺是如何工作的:
我们可以用一个KZG证明来表示一组数据[D₁...Dₙ],该证明是从数据派生出的多项式P的证明:具体而言,多项式P满足P(w) = D₁,P(w²) = D₂...P(wⁿ) = Dₙ。这里的w是一个“单位根”,满足wᴺ = 1,其中N是“评估域”的大小(所有操作都在一个有限域上进行)。
要对P进行“承诺”,我们创建一个椭圆曲线点com(P) = P₀ * G + P₁ * S₁ +...+ Pₖ * Sₖ。在这里:
G是曲线的生成点。
Pᵢ是多项式P的i次系数。
Sᵢ是“可信设置”中的第i个点。
要证明P(z) = a,我们创建一个“商(quotient)多项式”Q = (P - a) / (X - z),并创建一个对它的承诺com(Q)。只有在P(z)实际等于a时才能创建这样的多项式。
要验证一个证明,我们检查方程式Q * (X - z) = P - a,通过对证明com(Q)和多项式承诺com(P)进行椭圆曲线检查,我们检查e(com(Q), com(X - z)) ?= e(com(P) - com(a), com(1))。
一些重要的属性需要理解:
一个证明只是com(Q)的值,它占用48字节。
com(P₁) + com(P₂) = com(P₁ + P₂)。
这也意味着你可以将一个值“编辑”到现有的承诺中。假设我们知道Dᵢ当前为a,我们想将其设置为b,并且现有的承诺是com(P)。那么,对“P的承诺,但P(wⁱ) = b,并且其他评估未发生”变化的承诺,我们设置com(new_P) = com(P) + (b - a) * com(Lᵢ),其中Lᵢ是一个“拉格朗日多项式”,在wⁱ处等于1,在其他wʲ点处等于0。
为了高效地执行这些更新,每个客户端可以预先计算和存储所有N个拉格朗日多项式(com(Lᵢ))。在链上的合约中,存储所有N个承诺可能太多了,所以可以通过对com(Lᵢ)的集合(或哈希(com(Lᵢ))值)进行KZG承诺来代替,这样每当有人需要在链上更新树时,只需提供适当的com(Lᵢ)及其正确性的证明。
因此,我们有了一个结构,我们可以不断地向不断增长的列表末尾添加值,虽然有一定的大小限制(实际上,数亿个可能是可行的)。然后,我们将此作为我们管理(i)每个L2上存储的键列表的承诺(com(key list)和镜像到L1,以及(ii)存储在以太坊L1上的L2键承诺列表的承诺的数据结构。
保持承诺的更新可能成为核心L2逻辑的一部分,或者可以通过存款和提取桥接来在没有L2核心协议更改的情况下实现。
完整的证明需要:
持有密钥库的L2上的最新com(key list)(48字节)
com(key list)在存储所有键列表承诺的com(mirror_list)中的KZG证明(48字节)
你在com(key list)中的密钥的KZG证明(48字节,加上索引的4字节)
实际上,可以将两个KZG证明合并为一个,所以总大小只有100字节。
请注意一个细节:由于键密钥列表是一个列表,而不是像状态一样的键/值映射,密钥列表将不得不按顺序分配位置。密钥承诺合约将包含其自己的内部注册表,将每个密钥库映射到一个ID,并且对于每个密钥,它将存储hash(key,密钥库的地址)而不仅仅是key,以明确地向其他L2传达关于哪个密钥库的条目的信息。
这种技术的优势在于在L2上性能非常好。数据大小为100字节,比ZK-SNARK小约4倍,比Merkle证明要小得多。计算成本主要是一次大小为2的配对检查,约为119,000 gas。在L1上,数据不像计算那样重要,因此不幸的是,KZG比Merkle证明要昂贵一些。
Verkle树将如何工作?
Verkle树基本上涉及将KZG承诺(或更高效且使用较简单密码学的IPA承诺)叠加在彼此之上:要存储2⁴⁸个值,你可以对2²⁴个值的列表进行KZG承诺,其中每个值本身是对2²⁴个值的KZG承诺。Verkle树正在强烈考虑用于以太坊状态树,因为Verkle树可以用于保存密钥值映射而不仅仅是列表(基本上,你可以创建一个大小为2²⁵⁶的树,但初始为空,只有在实际需要填充时才会填充特定部分的树)。
Verkle树的样子
Verkle树的证明比KZG略长;它们可能会有几百字节长。它们也很难验证,特别是如果尝试将许多证明聚合成一个。
实际上,Verkle树应该被视为类似于Merkle树,但没有SNARKing的可行性更高(因为数据成本较低),并且在使用SNARKing时更便宜(因为证明者成本较低)。
Verkle树的最大优势在于数据结构的统一性:Verkle证明可以直接在L1或L2状态上使用,而无需覆盖结构,并且对于L1和L2使用完全相同的机制。一旦量子计算机成为问题,或者一旦证明Merkle分支变得足够高效,Verkle树可以使用适用于SNARK的哈希函数在原地替换为二进制哈希树。
聚合
如果N个用户进行N个交易(或更现实地说,N个ERC-4337 UserOperations)需要证明N个跨链声明,我们可以通过聚合这些证明来节省大量的gas:将这些交易组合成进入区块或打包到区块中的构建者可以创建一个单一的证明,同时证明所有这些声明。
这可能意味着:
N个Merkle分支的ZK-SNARK证明
KZG多证明
Verkle多证明(或Verkle多证明的ZK-SNARK)
在这三种情况下,每个证明的成本仅为几十万个gas。构建者需要为每个使用该区块的L2中的用户制作一个这样的证明;因此,为了对构建者有用,整个方案需要具有足够的使用量,以便在多个主要L2上的同一区块中往往至少有几个交易。
如果使用ZK-SNARK,主要的边际成本仅仅是在合约之间传递数字的“业务逻辑”,因此每个用户可能需要几千个L2 gas。如果使用KZG多证明,证明者需要为每个包含L2的keystore-holding L2添加48个gas,因此每个用户的方案边际成本将在此基础上再增加约800个L1 gas(而不是每个用户)。但是,这些成本远远低于不聚合的成本,后者不可避免地涉及每个用户超过10,000个L1 gas和数十万个L2 gas。对于Verkle树,你可以直接使用Verkle多证明,每个用户增加约100-200个字节,或者你可以创建Verkle多证明的ZK-SNARK,其成本与Merkle分支的ZK-SNARK类似,但证明成本要低得多。
从实现的角度来看,最好通过ERC-4337帐户抽象标准让捆绑器通过自定义方式聚合跨链证明。ERC-4337已经为构建者聚合UserOperations的部分提供了机制。甚至已经有一个用于BLS签名聚合的实现,这可以将L2上的gas成本降低1.5倍到3倍,具体取决于包括哪些其他形式的压缩。
早期版本ERC-4337 BLS聚合签名工作流
直接状态读取
最后一种可能性,仅适用于L2读取L1(而不是L1读取L2),这是修改L2,使其能够直接对L1上的合约进行静态调用。
这可以通过一种操作码或预编译来实现,它允许调用L1中的地址,gas和calldata,并返回输出,尽管因为这些调用是静态调用,它们不能实际上改变任何L1状态。L2已经意识到L1以处理存款,因此从技术上讲,没有任何阻止实现这种机制的根本原因(参见:Optimism提供支持静态调用进入L1的RFP)。
请注意,如果密钥库位于L1上,并且L2集成了L1静态调用功能,那么根本不需要证明!但是,如果L2不集成L1静态调用,或者如果密钥库位于L2上(一旦L1变得对用户使用甚至稍微使用都太昂贵时可能会发生这种情况),那么将需要证明。
L2如何获取最新的以太坊状态根
为了处理从L1到L2的消息(尤其是存款),所有的L2都需要一些功能来访问最新的L1状态。实际上,如果一个L2具有存款功能,那么你可以使用该L2原样将L1状态根移动到L2上:只需让L1上的合约调用BLOCKHASH操作码,并将其作为存款消息传递到L2。L2端可以接收到完整的区块头,并提取其状态根。然而,每个L2都最好有一种明确的方式来直接访问最新的L1全面状态或最近的L1状态根。
优化L2接收最新L1状态根的主要挑战是同时实现安全性和低延迟:
如果L2以延迟方式实现“直接读取L1”功能,只读取已最终确定的L1状态根,则延迟通常为15分钟,但在极端情况下,如不活跃泄漏(必须能够容忍),延迟可能长达几周。
L2确实可以设计成读取更近期的L1状态根,但因为L1可能发生回滚(即使是在单槽最终确定性的情况下,也可能在不活跃泄漏期间发生回滚),因此L2也需要能够回滚。从软件工程的角度来看,这在技术上是具有挑战性的,但至少Optimism已经具备了这种能力。
如果使用存款桥将L1状态根带入L2,则简单的经济可行性可能要求存款更新之间有较长的时间间隔:如果一个存款的全部成本为100,000个gas,并假设以太坊价格为$1800,手续费为200 gwei,每天将L1状态根带入L2一次,那么每个L2每天的成本为$36,或者每个L2每年的成本为$13,148,用于维护系统。如果延迟为一小时,这个成本将增加到每个L2每年$315,569。在最好的情况下,一些急于支付的富有用户会为更新费用支付,并使系统保持最新状态,以服务其他用户。在最糟糕的情况下,某个无私的参与者将不得不自己支付费用。
“预言机”(至少在一些DeFi人士眼中被称为“预言机”)在这里不是一个可接受的解决方案:钱包密钥管理是非常关键的底层功能,因此它应该最多依赖于一些非常简单、具有密码学信任的底层基础设施。
另外,反向(L1读取L2):
在optimistic rollup中,状态根需要一周的时间才能到达L1,这是由于欺诈证明的延迟。在零zkrollup中,由于证明时间和经济限制的结合,目前需要几个小时,但未来的技术将减少这一时间。
在L1读取L2的情况下,使用“预确认”(来自排序器、认证者等)并不是一种可接受的解决方案。钱包管理是一个非常关键的安全性低级功能,因此L2->L1通信的安全级别必须是绝对的:甚至无法通过接管L2验证者集合来推送错误的L1状态根。L1应该信任的唯一状态根是L2在L1上的状态根持有合约已经接受为最终的状态根。
有些跨链操作的速度对于许多DeFi用例来说太慢了;对于这些情况,我们确实需要更快速的桥接方案,但这些方案的安全性模型可能不够完善。然而,对于更新钱包密钥的用例,较长的延迟更可接受:你不是将交易延迟几个小时,而是将密钥更改延迟。你只需要将旧密钥保留更长时间。如果要更改密钥是因为密钥被盗,那么你将面临相当长的漏洞期,但可以通过一些措施来减轻风险,例如,钱包可以具备冻结功能。
总的来说,最小化延迟的最佳解决方案是L2以最佳方式实现直接从内部读取L1状态(或至少状态根)的能力,其中每个L2区块(或状态根计算日志)包含指向最近的L1区块的指针,因此如果L1回滚,L2也可以回滚。密钥库合约应该放置在主网上或是ZK rollup L2上,这样可以快速提交给L1。
其他链需要与以太坊保持多少连接以维护基于以太坊或L2根的钱包
出人意料地,这个连接并不需要太多。它甚至不需要成为一个正式的L2,如果是一个L3或validium,只要该链直接访问以太坊状态根,并在以太坊发生回滚时重新组织或硬分叉的技术和社会承诺。
一个有趣的研究问题是确定一个链与多个其他链(例如以太坊和Zcash)之间在多大程度上具有这种形式的连接。直接而幼稚的方法是可能的,即如果以太坊或Zcash回滚,密的链将不得不重组(并在以太坊或Zcash硬分叉时进行硬分叉),但然后你的社区将具有双重技术和政治依赖性(或者如果将你的链与许多其他链绑定,依赖性将更多)。基于ZK桥接的方案对51%攻击或硬分叉不具备鲁棒性。可能有更聪明的解决方案。
隐私保护
理想情况下,我们还希望保护隐私。如果你有许多由同一个密钥库管理的钱包,那么我们希望确保:
公众不知道这些钱包彼此相互关联。
社交恢复监护人不知道他们正在监护的地址是什么。
这会引起一些问题:
我们不能直接使用 Merkle 证明,因为它们不能保护隐私。
如果我们使用 KZG 或 SNARKs,那么证明需要提供验证密钥的盲化版本,而不泄露验证密钥的位置。
如果我们使用聚合,那么聚合器不应该以明文形式了解位置;相反,聚合器应该接收盲化证明,并有一种方式来聚合这些证明。
我们不能使用“轻量级版本”(仅使用跨链证明更新密钥),因为它会泄露隐私:如果由于更新过程导致多个钱包同时更新,时间上的泄露将暗示这些钱包可能相关。因此,我们必须使用“重量级版本”(每个交易都使用跨链证明)。
对于 SNARKs,解决方案在概念上很简单:证明默认是隐藏信息的,聚合器需要生成递归 SNARK 来证明这些 SNARKs。
这种方法的主要挑战是聚合需要聚合器创建递归 SNARK,而这在当前情况下速度相对较慢。
对于 KZG,我们可以以非索引泄露 KZG 证明的工作为起点(也可参考:Caulk 论文中对该工作的更正式版本)。然而,盲化证明的聚合是一个需要更多关注的开放问题。
不幸的是,直接从 L2 内部读取 L1 并不能保护隐私,尽管实现直接读取功能仍然非常有用,既可以最小化延迟,也可以用于其他应用程序。
总结
要实现跨链社交恢复钱包,最现实的工作流程是在一个地方维护一个密钥库(keystore),并在许多位置维护多个钱包(wallets),其中钱包要么(i)读取密钥库以更新其本地验证密钥视图,要么(ii)在验证每个交易时读取密钥库。
实现这一目标的一个关键因素是跨链证明。我们需要努力优化这些证明。最好的选择可能是ZK-SNARKs、等待出现的Verkle证明,或者自行构建的KZG解决方案。
长期而言,聚合协议将是必需的,聚合器将作为创建用户提交的所有UserOperations捆绑包的一部分生成聚合证明,以最小化成本。这可能需要与ERC-4337生态系统整合,尽管可能需要对ERC-4337进行一些修改。
L2应该优化以最小化从L2内部读取L1状态(或至少状态根)的延迟。L2直接读取L1状态是理想的,并可以节省证明空间。
钱包不仅可以部署在L2上,还可以放置在与以太坊连接较低的系统上(L3,甚至仅同意包括以太坊状态根并在以太坊回滚或硬分叉时重新组织或硬分叉的独立链)。
然而,密钥库应该放置在L1或高安全性的ZK rollup L2上。放置在L1上可以节省很多复杂性,尽管从长远来看,即使这也可能太昂贵,因此需要在L2上放置密钥库。
保护隐私将需要额外的工作并增加一些困难。然而,我们应该朝向保护隐私的解决方案发展,并确保我们提出的任何解决方案都能与保护隐私保持前向兼容。
微信扫描关注公众号,及时掌握新动向
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场