V神博客:深入了解钱包和其他应用案例的Layer2跨层读取

'V神博客:Layer2跨层读取钱包和其他应用案例的深入了解'

作者:Vitalik Buterin;编译:布噜说

在《The Three Transitions》一文中,以太坊创始人Vitalik Buterin明确指出了“主网(下文简称L1)+第2层跨链(下文简称cross-L2)支持”、“钱包安全”和“隐私”作为生态系统堆栈所必需的重要功能,而不仅仅是一些附加的组件,这些功能应由单独的钱包提供。

而在本文中,Vitalik Buterin着眼于探讨如何更轻松地实现从L2读取L1的数据,或者从L1读取L2的数据,以及如何更轻松地从一个L2读取另一个L2的数据。

Vitalik Buterin指出,解决上述问题的关键在于实现资产与密钥库的分离架构。这个技术在扩容以外的领域也有非常有价值的用例,比如L1和L2之间资产的移动互通。

这样做的目标是什么?

一旦L2成为主流,用户将能够在多个L2上拥有资产,也可能在L1上拥有资产。

一旦智能合约钱包成为主流,现在常见的“密钥”将不再被使用。

而一旦这两件事情同时发生,用户就会需要一种不需要大量交易的方法,来更换不同账户的密钥。

尤其是,我们需要一种方法来处理那些“反事实设定”的地址(也可以理解为“假设地址”):这是一些尚未在链上“注册”的地址,但仍需要接收并安全地持有资产。

事实上,我们都依赖于这种“反事实设定”的地址:当用户第一次使用以太坊时,用户可以生成一个ETH地址,他人可以向这个账户支付,而无需在区块链上“注册”该地址(但会需要支付交易费用,因此需要持有若干ETH)。

对于外部账户(EOA)而言,其实所有的地址都是从“反事实设定”的地址开始的。

对于智能合约钱包,“反事实设定”的地址仍然是可能的,这在很大程度上要归功于CREATE2,它允许您拥有一个ETH地址,只能由与特定哈希值匹配的智能合约代码填充。

EIP-1014(CREATE2)地址计算算法

然而,引入智能合约钱包也带来了新的挑战:访问密钥可能发生变化。这个变化在于,地址是initcode的哈希值,只能包含钱包的初始验证密钥,而当前的验证密钥将存储在钱包的存储中,但该存储记录不会自动转移到其他L2中。

如果一个用户在许多L2上都有地址,那么资产与密钥存储分离架构可以帮助用户更改他们的密钥。

这个分离架构的结构是:每个用户都有(i)一个“密钥存储合约”(在L1或特定的L2链上),它存储了所有钱包的验证密钥以及更改密钥的规则,以及(ii)在L1和许多L2链上的“钱包合约”,它们通过跨链读取来获取验证密钥。

资产与密钥存储分离架构有两种实现方法:

轻量级版本(即仅检查更新密钥):每个钱包在本地存储验证密钥,并包含一个可调用的函数来检查密钥库当前状态的跨链证明,并更新本地存储的验证密钥以匹配。在某个L2上首次使用钱包时,调用该函数从密钥库获取当前的验证密钥是必需的。

  • 优点:对跨链证明的使用较为审慎,不会出现过高的网络操作费用。所有资产只能通过当前密钥使用,因此仍然保证了安全性。
  • 缺点:需要更改验证密钥时,必须在密钥库和已初始化的每个钱包上进行链上密钥更改,可能需要消耗大量的Gas费用。

完整版本(即每个交易都检查):每笔交易都需要一个跨链证明,显示密钥库中的当前密钥。

  • 优点:系统复杂性较低,并且密钥库更新迅速。
  • 缺点:单个交易的网络操作费用较高,不容易与ERC-4337兼容,目前ERC-4337不支持在验证期间跨合约读取可变对象。

什么是跨链证明?

为了展示跨链证明的复杂性,我们选取了一种最复杂的应用场景作为展示解释这个技术原理,这个复杂的应用场景如下:密钥存储在一个L2上,而钱包在另一个L2上。如果钱包上的密钥库在L1上,那么只需要此设计的一半。

假设密钥库在Linea上,钱包在Kakarot上。钱包密钥的完整证明过程则需要包括:

  • 证明当前Linea状态根的证明,给定Kakarot知道的当前以太坊状态根。
  • 证明密钥库中当前密钥的证明,给定当前Linea状态根。

这里有两个主要的棘手的实现问题:「需要使用什么样的证据?(是默克尔证明吗?还是别的什么?)」以及「L2如何学习最近的L1状态根?」或者,「L1如何学习L2的状态根?」那么,在这两种情况下,一方发生某事件后,到另一方能够提供证明之间,会有多长的延迟时间?

我们可以使用哪些证明方案?

主要有五种方法可供选择:

  • Merkle证明
  • 通用ZK-SNARKs
  • 特殊目的证明(例如,使用KZG)
  • Verkle证明,介于KZG和ZK-SNARKs之间,既考虑基础设施工作量又考虑成本
  • 没有证明,依赖直接状态读取

就所需的基础设施工作和用户成本而言,大致可将它们进行如下排列比较:

「聚合」是指将每个区块中用户提供的所有证明聚合成一个大的元证明,将它们合并在一起。这对于SNARKs和KZG是可行的,但对于Merkle分支来说不行。事实上,只有当方案拥有大量用户时,「聚合」才能体现价值。

Merkle证明是如何工作的?

这个问题很简单,可以直接按照上一节的图表。每个「证明」(假设是将一个L2证明为另一个L2,这是难度最大的一种应用场景)将包括:

  • 一个Merkle分支,证明了持有L2键库的状态根,根据L2所知道的以太坊的最新状态根。持有L2键库的状态根存储在已知地址(代表L2的L1合约)的已知存储槽中,因此可以将路径硬编码。
  • 一个Merkle分支,证明了当前的验证密钥,根据持有L2键库的状态根。同样,验证密钥存储在已知地址的已知存储槽中,因此路径可以硬编码。

然而,以太坊的状态证明很复杂,但是有一些库可以用来验证它们,如果使用这些库,这个机制并不太复杂。

不过,更大的挑战是成本问题。Merkle证明很长,而LianGuaitricia树比必要的就是长3.9倍,远高于目前每笔交易2.1万个Gas Fee的基本价格。但是,如果在L2上验证证明,则差异会变得更糟。L2内部的计算很便宜,因为计算是在链下完成的,并且是在节点数量远少于L1的生态系统里完成。我们可以通过查看L1 Gas Fee成本和L2 Gas Fee成本之间的比较来计算这意味着什么:

当下,如果是较为简单的发送操作,L1网络上的成本大约是L2的15~25倍,而Token交换的成本则大约是L2的20~50倍。简单的发送操作,数据量较大;而交换操作对于算力的要求更高,因此,交换操作是一个更好的基准来近似L1计算与L2计算的成本。

综合考虑以上情况,如果我们假设L1计算成本和L2计算成本之间的成本比为30倍,这似乎意味着将Merkle证明放在L2上的成本可能相当于大约五十个常规交易。

当然,使用二进制Merkle树可以减少成本,约为4倍,但即使如此,在大多数情况下,成本仍然会过高 ,而且如果我们愿意放弃与以太坊当前的六进制状态树的兼容性,可能还会寻求更好的选择。

ZK-SNARK证明是如何工作的?

从概念上讲,ZK-SNARK的使用也很容易理解:您只需将上图中的Merkle证明替换为证明这些Merkle证明存在的ZK-SNARK。一个ZK-SNARK的计算量约为400,000 Gas Fee,约400字节;一个基本事务需要21,000个Gas Fee和100个字节。

因此,从计算角度看,ZK-SNARK的成本是现在基本交易成本的19倍;从数据角度看,ZK-SNARK的成本是现在基本交易成本的4倍,是未来基本交易成本的16倍。

这些数字与Merkle证明相比有了巨大的改进,但仍然相当昂贵。有两种方法可以改善这种情况:(i)特殊用途的KZG证明,或(ii)聚合,类似于ERC-4337聚合。

特殊用途的KZG证明如何工作?

首先,回顾一下KZG承诺的工作原理:

[D_1 …D_n]表示一组数据,通过这组数据导出多项式KZG证明。

具体来说,多项式P,其中P(w)=D_1,P(w²)=D_2 …P(wⁿ)=D_n. w 这里是“统一根”,对于某些评估域大小N,wN=1的值(这一切都是在有限域中完成的)。

为了“提交”到P,我们创建一个椭圆曲线点com(P)=P₀ * G + P₁ * S₁ + … + Pk * Sk。这里:G是曲线的生成器点,Pi是多项式P的第i次系数,Si是可信设置中的第i个点。

而为了证明P(z)=a,我们创建一个商多项式Q=(P – a)/(X – z),并创建一个承诺com(Q)。只有当P(z)实际上等于a时,才有可能创建这样的多项式。

为了验证证明,我们通过对证明com(Q)和多项式承诺com(P)进行椭圆曲线检查来检查方程Q * (X – z)=P – a:我们检查e(com(Q), com(X – z)) ≠ e(com(P) – com(a), com(1))。

还需要了解的一些关键属性包括:证明只是com(Q)值,即48个字节,com(P₁) + com(P₂) = com(P₁ + P₂)。这也意味着您可以将值“编辑”为现有合约。

假设我们知道D_i当前是a,我们希望将其设置为b,并且对D的现有承诺是com(P)。承诺“P,但P(wⁱ)=b,并且没有其他评估更改”,然后我们设置com(new_P)=com(P)+(b-a)*com(Li),其中Li是“拉格朗日多项式”,在wⁱ处等于1,在其他wj点处等于0。

为了有效地执行这些更新,每个客户端都可以预先计算和存储对拉格朗日多项式(com(Li))的所有N个承诺。在链上合约中,存储所有N个承诺可能太多了,所以你可以对com(L_i)值集做出KZG承诺,所以每当有人需要更新链上的树时,他们可以简单地向适当的com(L_i)提供其正确性的证明。

因此,有一个结构可以继续将值添加到不断增长的列表的末尾,但有一定的大小限制。然后,使用这个结构作为数据结构(i)对每个L2上的密钥列表的承诺,存储在该L2上并镜像到L1,以及(ii)对L2密钥承诺列表的承诺,存储在以太坊L1上并镜像到每个L2。

保持承诺更新可以成为核心L2逻辑的一部分,也可以通过存款和撤回桥接实现,而无需更改L2核心协议。

一份完整的证明所需的内容如下:

  • 存放L2上密钥库的最新com(密钥列表)。
  • 将com(密钥列表)作为com(镜像列表)中的值的KZG证明,com(镜像列表)是所有密钥列表承诺的列表。
  • 将用户的密钥在com(密钥列表)中进行KZG证明。

事实上,上述两个KZG证明可以合并为一个,总大小只有100字节。

请注意一个细节:由于密钥列表是一个列表,而不是像状态那样的键/值映射,密钥列表必须按顺序分配位置。密钥承诺合约将包含其自己的内部注册表,将每个密钥库映射到一个ID,并且对于每个密钥,它将存储hash(key,密钥库的地址)而不仅仅是key,以明确地告知其他L2关于特定条目所指的密钥库。

这种技术的优点是在L2上性能非常好。比ZK-SNARK短约4倍,比Merkle证明短得多。计算成本大约为119,000 Gas Fee。在L1上,算力比数据更重要,因此KZG比Merkle证明要稍微昂贵一些。

Verkle树如何工作?

Verkle树本质上涉及将KZG承诺堆叠在一起:要存储2⁴⁸值,可以对2²⁴值列表做出KZG承诺,每个值本身都是KZG对2²⁴值的承诺。Verkle树被考虑用于以太坊状态树,因为Verkle树可以用来保存键值映射。

Verkle树中的证明比KZG证明更长,它们可能有几百个字节长。实际上,Verkle树应该被认为是像Merkle树,但如果没有SNARKing更可行,但SNARKing被证明有更低的证明成本。Verkle树的最大优点是可以协调数据结构:因此可以直接用于L1或L2,没有叠加结构,并且对L1和L2使用完全相同的机制。一旦量子计算机成为一个问题,或者一旦证明Merkle分支变得足够高效,Verkle树就有了更多的用武之地。

聚合

如果N个用户做了N笔交易,需要证明N个跨链索赔,我们可以通过聚合这些证明来节省大量的Gas Fee,这可能意味着:

  • 一个N个Merkle分支的ZK-SNARK证明
  • 一个KZG多重证明
  • 一个Verkle多重证明(或一个多重证明的ZK-SNARK)

在所有这三种情况下,每个证明只需花费几十万Gas Fee。开发者需要在每个L2上为该L2的用户制作一个这样的证明;因此,为了使这个证明有用,整个计划需要有足够的使用量,以至于在多个主要L2的同一区块内经常有至少几个交易。

如果使用ZK-SNARKs,每个用户可能需要花费几千个L2 Gas Fee。如果使用KZG多重证明,验证者需要为该区块内使用的每个持有钥匙库的L2增加48个Gas Fee。

不过,这些成本比不聚合的成本要低得多,后者不可避免地涉及到每个用户超过10000个L1 Gas Fee和数十万个L2 Gas Fee。

对于Verkle树,用户可以直接使用Verkle多证明,每个用户增加大约100~200字节,或者你可以做一个Verkle多重证明的ZK-SNARK,它的成本与Merkle分支的ZK-SNARK相似,但证明起来明显便宜。

从实施的角度来看,让捆绑者通过ERC-4337账户抽象标准聚合跨链证明可能是最好的。ERC-4337已经有一个机制,让构建者以自定义的方式聚合User Operations的部分。甚至有一个针对BLS签名聚合的实现,这可以将L2的Gas Fee降低1.5倍到3倍。

直接读取状态

最后一种可能,也是只适用于L2读L1(而不是L1读L2)的一种可能,就是修改L2,让它们直接对L1的合约进行静态调用。

这可以通过一个操作码或预编译来实现,它允许调用L1,你提供目标地址、气体和calldata,然后它返回输出,尽管由于这些调用是静态调用,它们实际上不能改变任何L1状态。L2必须知道L1的情况才能处理存款,所以没有什么根本性的东西可以阻止这种东西的实现;这主要是一个技术实现上的挑战。

请注意,如果密钥库在L1上,并且L2整合了L1的静态调用功能,那么就根本不需要证明。

但是,如果L2没有整合L1静态调用,或者如果密钥库在L2上,那么就需要证明了。

L2如何学习最近的以太坊状态根?

上述所有方案都要求L2访问最近的L1状态根或整个最近的L1状态。

事实上,如果L2具有存入功能,那么您可以按原样使用该L2将L1状态根移动到L2上的合约中:只需让L1上的合约调用BLOCKHASH操作码,并将其作为资产存入的消息传递给L2。可以在L2端接收完整的块标头,并提取其状态根。

但是,每个L2最好都有明确的方式来直接访问完整的最新L1状态或最近的L1状态根。

优化L2接收最新L1状态根的方式的主要挑战是同时实现安全性和低延迟:

  • 如果L2缓慢实现直接读取L1功能,只读取最终的L1状态根,那么延迟通常为15分钟,但在一些极端情况下,延迟可能是几周。
  • L2绝对可以设计为读取更新的L1状态根,但由于L1可以恢复(即使具有单插槽终结性,在非活动泄漏期间也会发生恢复),L2也需要能够恢复。从软件工程的角度来看,这在技术上具有挑战性。
  • 如果使用桥将L1状态根引入L2,那么资产更新需要花费很长时间,在最好的情况下,不断有用户支付更新费用,并使系统为其他人保持最新状态。
  • 不过,“预言机”(Oracles)在这里不是一个可接受的解决方案:钱包密钥管理是一个非常安全的关键低级功能,因此它最多应该依赖于几个非常简单的、无需加密信任的低级基础设施。

此外,对于许多DeFi用例来说,其中一些用于无信任跨链操作的速度慢得令人无法接受。然而,对于更新钱包密钥的用例,更长的延迟更容易接受,因为不是延迟交易,是延迟密钥更改。用户只需要将旧密钥保留更长时间即可。如果用户因为密钥被盗而更改密钥,那么确实有很长一段时间的漏洞,但可以缓解,例如通过具有冻结功能的钱包。

最终,最好的延迟最小化解决方案是让L2以最佳方式实现对L1状态根的直接读取,其中每个L2块(或状态根计算日志)包含一个指向最新L1块的指针,因此如果L1恢复,L2也可以恢复。密钥库合约应放置在主网上或ZK-rollup的L2上,以便可以快速提交到L1。

另一个链需要多少与以太坊有多少连接,才能持有密钥库存储在以太坊或L2的钱包?

令人惊讶的是,没有那么多。实际上,它甚至不需要是一个Rollup。

如果它是一个L3,或者是一个validium,那么在那里存放钱包也是可以的,只要用户在L1或ZK-rollup上存放密钥存储,确实需要能够直接访问以太坊的状态根,并且愿意在以太坊重构时,在以太坊硬分叉时进行硬分叉。

基于ZK桥的方案有吸引人的技术特性,但它们有一个关键的弱点,即它们对51%攻击或硬分叉不健全。

保护隐私

理想情况下,用户还希望保护隐私。如果一个用户有许多由同一个密钥库管理的钱包,那么他们希望确保:

  • 不让公众知道这些钱包都是相互连接的。
  • 社交恢复监护人不会了解他们所监护的地址是什么。

但这就产生了以下问题:

  • 我们不能直接使用Merkle证明,因为它们不能保护隐私。
  • 如果我们使用KZG或SNARKs,那么证明需要提供验证密钥的盲版,而不泄露验证密钥的位置。
  • 如果我们使用聚合,那么聚合器就不应该以明文的方式了解位置;相反,聚合器应该接收盲证明,并有办法聚合这些证明。
  • 我们不能使用「轻量级版本」(仅在更新密钥时使用跨链证明),因为这会造成隐私泄露:如果许多钱包由于更新程序而同时更新,那么时间上就会泄露这些钱包可能相关的信息。因此,我们必须使用「完整版本」(对每笔交易进行跨链证明)。

对于SNARKs,解决方案在概念上很简单:默认情况下证明是信息隐藏的,聚合器需要产生递归SNARK来证明SNARKs。

这种方法目前面临的主要挑战是:聚合需要聚合器创建一个递归的SNARK,速度上相当慢。

对于KZG,我们可以使用非索引揭示KZG证明的工作。然而,盲证的聚合是一个开放的问题,需要更多的关注。

不过,虽然从L2内部直接读取L1并不能保护隐私,但实现这个直接读取功能仍然将非常有用,不仅因为可以最大限度地减少延迟,还可以用于其他更多用例。