在 L2 成为主流后, 在多个L2 的钱包管理是一个需要解决的问题,使用keystore合约和钱包合约分离的架构是一个可行的方式, Vitalik 在文章中探讨了 3 种可能跨链证明方法来实现该架构。
在这篇关于三个转变的文章中,我概述了一些关键的原因,为什么开始明确考虑L1 + 跨 L2支持、钱包安全和隐私是生态系统堆栈的必要基本功能,而不是把这些东西都建成由个别钱包单独设计的附加功能。
本文将更直接地关注一个特定子问题的技术方面:如何让从L2读取L1,从L1读取L2,或从另一个L2读取L2更容易。解决这个问题对于实现资产/keystore分离架构至关重要,但它在其他领域也有宝贵的使用场景,最明显的是优化可靠的跨L2调用,包括在L1和L2之间移动资产等使用场景。
译者注:本文难度系数较高,如果只对结论感兴趣,可以跳到总结
一旦L2成为主流,用户将拥有跨越多个L2的资产,可能还有L1。一旦智能合约钱包(多签,社交恢复或其他)成为主流,访问一些账户所需的密钥将随着时间的推移而改变,旧的密钥将需要不再有效。一旦这两件事发生,用户将需要有一种方法来改变有权限访问许多账户的key,这些账户存在许多不同的链,而不需要进行极多的交易。
特别是,我们需要一种方法来处理反事实的地址:尚未以任何方式在链上 "注册" 的地址,但它们仍然需要接收和安全地持有资金。我们都依赖于反事实地址:当你第一次使用以太坊时,你能够生成一个ETH地址,别人可以用它来支付你,而不需要在链上 "注册" 地址(这将需要支付手续费,因此需要已经持有一些ETH)。
EOA用户地址,所有地址一开始都是反事实的地址。通过智能合约钱包,反事实的地址仍然是可能的,这主要归功于CREATE2,它允许你有一个ETH地址,只能由拥有与特定哈希值相匹配的代码的智能合约来填充。
EIP-1014(CREATE2)地址计算算法.。
然而,智能合约钱包引入了一个新的挑战:访问密钥改变的可能性。地址是initcode
的哈希值,只能包含钱包的初始验证密钥。当前的验证密钥将被存储在钱包的存储中,但该存储记录不会神奇地传播到其他L2。
如果一个用户在许多L2上有许多地址,包括他们所在的L2不知道的地址(因为他们是反事实的),那么似乎只有一种方法允许用户改变他们的key:资产/keystore 分离架构。每个用户都有(i)一个 "keystore合约 "(在L1或一个特定的L2上),它存储了所有钱包的验证密钥以及更改密钥的规则,以及(ii) "钱包合约 "在L1和许多L2上,它跨链读取以获得验证密钥。
有两种方法来实现这一点:
轻型版本(只检查更新 key):每个钱包都在本地存储验证密钥,并包含一个可以被调用的函数,以检查keystore的当前状态的跨链证明,并更新其本地存储的验证密钥,使之与之匹配。当一个钱包第一次在一个特定的L2上使用时,调用该函数从keystore中获得当前的验证密钥是强制性的。
优势:很少使用跨链证明,所以如果跨链证明很贵也没关系。所有的资金都只能用当前的密钥来花费,所以它仍然是安全的。
劣势:要改变验证key,你必须在keystore和每个已经初始化的钱包(虽然不是反事实的钱包)中进行链上key改变。这可能会耗费大量的Gas。
重型版本(检查每个tx):每笔交易都需要一个跨链证明,显示当前在keystore中的key。
优点:更少的系统复杂性,并且keystore的更新很便宜。
劣势:每笔交易都很昂贵,所以需要更多的工程来使跨链证明变得可接受的便宜。也不容易与ERC-4337兼容,它目前不支持在验证期间跨合约读取可变对象。
为了显示全部的复杂性,我们将探讨最困难的情况:keystore 在一个L2上,而钱包在另一个L2上。如果钱包上的keystore是在L1上,那么只需要这个设计的一半。
让我们假设keystore在Linea,而钱包在Kakarot。钱包的key的完整证明包括:
这里有两个主要的棘手的实施问题:
我们使用什么样的证明?(是默克尔证明吗?其他的?)
L2 如何知道最近L1(以太坊)的状态根 (或者,正如我们将看到的,可能是完整的L1状态)?或者说,L1是如何 知道 L2的状态根的?
有五个主要的选择:
就所需的基础设施工作和用户的成本而言,我对它们的排名大致如下:
”聚合“指的是将每个区块内由用户提供的所有证明聚合成一个大的元证明,将所有这些证明结合起来。这对SNARKs和KZG来说是可能的,但对Merkle分支来说是不可能的(你也许可以把Merkle分支组合起来,但它只能为你节省log(txs per block) / log(total number of keystores)
,在实践中可能是15-30%,所以它可能不值得花费)。
聚合只有在方案有相当数量的用户时才变得值得,所以现实中,第一版的实现可以不进行聚合,而在第二版实现。
这个很简单:直接按照上一节的图。更确切地说,每个 "证明"(假设在最大难度的情况下在一个L2证明另一个L2)将包含:
不幸的是,以太坊的状态证明很复杂,但存在验证库,如果你使用这些库,这个机制的实现就不会太复杂。
更大的问题是成本。Merkle证明很长,很不幸,而Patricia树比必要的时间还长3.9倍(准确地说:一个理想的Merkle证明到一棵持有N
对象的树是32 * log2(N)
字节长,因为以太坊的Patricia树每个子树有16个叶子,这些树的证明是32 * 15 * log16(N) ~= 125 * log2(N)
字节长)。在一个大约有2.5亿(~2²⁸)个账户的状态下,这使得每个证明125 * 28 = 3500
字节,或大约56000个Gas,还需加上解码和验证哈希的额外费用。
两个证明加起来最终会花费大约100,000到150,000个Gas(不包括签名验证,如果这是每笔交易使用的)-- 明显高于目前每笔交易21,000个Gas的基础。但是,如果证明是在L2上验证的,需要的 Gas 差距就更大了。L2 内部的计算很便宜,因为计算是在链外和生态系统中进行的,L2节点比L1少很多。但另一方面,数据则必须被发布到L1上。因此,比较对象不是21000个Gas 对 15 万个Gas;而是 21000 个 L2 Gas 对 10 万个 L1 Gas。
我们可以通过观察L1Gas成本和L2Gas成本之间的比较来计算这意味着什么:
目前,在简单转账,L1的成本是L2的15-25倍,而在代币兑换方面则是20-50倍。简单转账是数据相对较重,但兑换的计算量要大得多。因此,兑换是一个更好的基准,更接近 L1 计算与 L2 计算的成本。考虑到所有这些,如果我们假设L1计算成本和L2计算成本之间有30倍的成本比率,这似乎意味着将一个Merkle证明放在L2上的成本可能相当于50个常规交易。
当然,使用二进制Merkle树可以降低成本~4倍,但即使如此,成本在大多数情况下还是会太高--如果我们愿意做出牺牲,不再与以太坊当前的十六进制状态树兼容,我们不妨寻求更好的选择。
从概念上讲,ZK-SNARK 的使用也很容易理解:你只需用一个Merkle证明存在(ZK-SNARK证明)来替换 上图中的Merkle证明。一个ZK-SNARK需要花费~400,000 Gas的计算,以及大约400个字节(比较一下:21,000个Gas和100个字节的基本交易,在未来通过压缩可减少到~25个字节。因此,从计算的角度来看,一个ZK-SNARK的成本是今天基本交易成本的19倍,而从数据的角度来看,一个ZK-SNARK的成本是今天基本交易的4倍,是未来基本交易成本的16倍。
这些数字是对Merkle证明的巨大改进,但它们仍然相当昂贵。有两种方法可以对此进行改进:(i) 特殊用途的KZG证明,或者(ii) 聚合,类似于ERC-4337聚合,但使用更花哨的数学方法。我们可以对这两种方法进行研究。
警告,这一节比其他章节更有数学性。这是因为我们要超越通用的工具,建立一些特殊用途的工具,以使其更便宜,所以我们必须更多地 "在底层 "进行研究。如果你不喜欢深奥的数学,请直接跳到 下一节 。
首先,回顾一下KZG承诺的工作方式:
我们可以用从数据中得出的*多项式的KZG证明来表示一组数据[D_1 ... D_n]
:具体来说,就是多项式P
,其中P(w)=D_1
,P(w²)=D_2
...P(wⁿ) = D_n
。w
在这里是一个 统一根
,一个在某个评价域大小N
中wᴺ=1
的值(这都是在一个有限域中完成的)。
为了 承诺
到P
,我们创建一个椭圆曲线点`com(P) = P₀ G + P₁ S₁ + ... + Pₖ * Sₖ```
在这里:
G
是曲线的生成点。Pᵢ
是多项式P
的第i度系数Sᵢ
是信任的设置中的第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(Lᵢ)
,其中Lᵢ
是一个 拉格朗日多项式
,在wⁱ
等于1
,在其他wʲ
点等于0。N
对拉格朗日多项式的承诺(com(Lᵢ)
)可以预先计算并由每个客户端存储。在链上合约内部,存储所有的N
个承诺可能太多,因此可以用KZG承诺对com(L_i)
(或hash(com(L_i)
)的值集合,因此每当有人需要更新链上树,他们可以简单地提供适当的com(L_i)
,并证明其正确性。因此,我们有一个结构,我们可以在一个不断增长的列表的末尾不断添加值,尽管有一定的大小限制(现实中,数以亿计的值可能是可行的)。然后,我们使用它作为我们的数据结构来管理(i)对每个L2上的密钥列表的承诺,存储在该L2上并镜像到L1上,以及(ii)对L2密钥承诺列表的承诺,存储在以太坊 L1上并镜像到每个L2。
保持承诺的更新可以成为L2核心逻辑的一部分,也可以通过存款和提款桥在不改变L2核心协议的情况下实现。
因此,一个完整的证明将需要:
com(key list)
(48字节)com(key list)
是com(mirror_list)
内的一个值的KZG证明,即对所有key列表comitments的承诺(48 bytes)com(key list)
中(48字节,加上4字节的索引)。实际上可以将两个KZG证明合并为一个,所以我们得到的总大小只有100字节。
请注意一个微妙之处:因为密钥列表是一个列表,而不是像状态那样是一个键/值映射,所以密钥列表将不得不按顺序分配位置。密钥承诺合约将包含它自己的内部注册表,将每个keystore映射到一个ID,对于每个密钥,它将存储hash(key, address of the keystore)
,而不仅仅是key
,以便毫不含糊地向其他L2传达哪个keystore的特定条目。
这种技术的好处是,它在 L2 上的表现非常好。数据是100字节,比ZK-SNARK短4倍,比Merkle证明短很多。计算成本主要是一个规模为2的配对检查,或者大约119,000个Gas。在L1上,数据不如计算重要,因此不幸的是,KZG比Merkle证明要贵一些。
Verkle 树本质上涉及到将KZG承诺(或IPA承诺,这可能更有效,并使用更简单的密码学)相互堆叠:为了存储2⁴⁸值,你可以对一个2²⁴值的列表做出KZG承诺,每个列表本身就是对2²⁴值的KZG承诺。Verkle树正在被强烈考虑用于以太坊状态树,因为Verkle树可以用来保存键值图,而不仅仅是列表(基本上,你可以做一个大小为2²⁵⁶的树,但开始时是空的,只有在你真正需要填充的时候才填入树的特定部分)。
一个Verkle树是什么样子的。在实践中,对于基于IPA的树,你可能给每个节点的宽度为256 == 2⁸,对于基于KZG的树,宽度为2²⁴。
Verkle树中的证明比KZG要长一些;它们可能有几百个字节长。它们也很难验证,特别是如果你试图将许多证明汇总到一个证明中。
现实上,Verkle树应该被认为是像Merkle树一样,但是在没有SNARKing的情况下更可行(因为数据成本较低),而在有SNARKing的情况下更便宜(因为验证者成本较低)。
Verkle树最大的优点是可以协调数据结构:Verkle证明可以直接在L1或L2状态上使用,不需要叠加结构,并且对L1和L2使用完全相同的机制。一旦量子计算机成为一个问题,或者一旦证明Merkle分支变得足够高效,Verkle树就可以被一个带有合适的SNARK友好哈希函数的二进制哈希树所取代。
如果N个用户做了N个交易(或者更现实的说,N个ERC-4337 UserOperation),需要证明N个跨链 Claim ,我们可以通过聚合这些证明来节省大量的Gas:将这些交易合并成一个区块或捆绑成一个区块的构建者可以创建一个单一证明,同时证明所有这些 Claim。
这可能意味着:
在所有这三种情况下,每个证明只需花费几十万Gas。builder 需要在每个L2上为该L2的用户制作一个这样的证明;因此,为了使这个证明有用,整个计划需要有足够的使用量,在同一个区块内在多个主要L2上经常有至少几个交易。
如果使用ZK-SNARKs,主要的边际成本只是在合约之间传递数字的 "业务逻辑",所以每个用户可能有几千个L2 Gas。如果使用KZG多重证明,验证者需要为该区块内使用的每个持有keystore的L2增加48个Gas,所以每个用户的方案的边际成本将在此基础上再增加每个L2(不是每个用户)的~800个L1Gas。但这些成本比不聚合的成本要低得多,因为后者不可避免地涉及到每个用户超过10000个L1 Gas 和数十万个L2 Gas。对于 Verkle 树,你可以直接使用 Verkle 多证明,每个用户增加大约100-200字节,或者你可以做一个 Verkle 多证明的 ZK-SNARK,它的成本与 Merkle 分支的 ZK-SNARK 相似,但证明起来明显更便宜。
从实施的角度来看,可能最好是让捆绑者通过ERC-4337账户抽象标准来聚合跨链证明。ERC-4337已经有一个机制,让构建者以自定义的方式聚合UserOperation的部分。甚至还有一个针对BLS签名聚合的实现,这可以将L2的Gas成本降低1.5倍到3倍,这取决于包括哪些其他形式的压缩。
图自BLS钱包实现推文,显示了在ERC-4337的早期版本中BLS聚合签名的工作流程。聚合跨链证明的工作流程可能看起来非常相似。
最后一种可能性,也是只适用于L2读L1(而不是L1读L2)的可能性,就是修改L2,让它们直接对L1的合约进行静态调用。
这可以通过一个操作码或预编译来实现,它允许调用 L1,你提供目标地址、Gas和calldata,并返回输出,尽管由于这些调用是静态调用,它们实际上不会改变任何L1状态。L2必须知道L1的情况才能处理存款,所以没有什么根本性的东西可以阻止这种东西的实现;这主要是一个技术实现的挑战(见:这个RFP来自Optimism,支持静态调用到L1。
请注意,如果keystore在L1上,并且 L2 集成了 L1 的静态调用功能,那么就根本不需要证明! 然而,如果L2没有集成L1静态调用功能,或者如果keystore在L2上(它最终可能必须在L2上,一旦L1变得太贵,用户甚至无法使用一点点),那么就需要证明了。
上面的所有方案都要求L2访问最近的L1状态根,或者整个最近的L1状态。幸运的是,所有L2都已经有一些功能来访问最近的L1状态了。这是因为它们需要这样的功能来处理从L1到L2的消息,最明显的是存款。
事实上,如果一个 L2 有存款功能,那么你就可以使用这个L2来把L1的状态根移到L2的合约中:只要让L1的合约调用BLOCKHASH
操作码,并把它作为一个存款消息传递给L2。完整的块头可以被接收,其状态根也可以在L2端被提取出来。然而,对于每个L2来说,最好有一个明确的方法来直接访问完整的最近的L1状态,或者最近的L1状态根。
优化L2接收最近L1状态根的方式的主要挑战是同时实现安全和低延迟:
此外,在相反的方向(L1 读取 L2):
对于许多defi使用场景来说,这些无信任的跨链操作的一些速度是不可接受的;对于这些使用场景,你确实需要更快的桥接和更不完善的安全模型。然而,对于更新钱包key的使用场景来说,更长的延迟是可以接受的:你不是延迟交易几个小时,你是延迟key的修改。你只是要把旧的key保留更长的时间。如果你因为key被盗而更换key,那么你确实有一段相当长的脆弱期,但这是可以缓解的,例如通过钱包的 "冻结" 功能。
最终,最好的延迟最小化解决方案是 L2 以最佳方式实现对L1状态根的直接读取,其中每个L2块(或状态根计算日志)包含一个指向最近L1块的指针,所以如果L1回退,L2也可以回退。Keystore 合约应该放在以太坊主网上,或者放在ZK-rollups L2上,并可以快速提交给L1。
L2链的区块不仅可以对以前的L2区块有依赖性,还可以对L1区块有依赖性。如果L1 回退过去的链接,L2也会回退。值得注意的是,这也是早期(Dank之前)版本的分片设想的工作方式;代码见这里。
令人惊讶的是,没有那么多。事实上,它甚至不需要是一个 Rollup :如果它是一个L3,或一个validium,那么在那里持有钱包是可以的,只要你在L1或ZK Rollup 上持有keystore。你需要的是该链能直接访问以太坊的状态根,以及技术和社会承诺,愿意在以太坊重组时重组,并在以太坊硬分叉时硬分叉。
一个有趣的研究问题是确定一条链在多大程度上有可能与多个其他链(如以太坊和Zcash)建立这种形式的联系。天真地做是可能的:如果以太坊或 Zcash重组,你的链可以同意重组(如果以太坊或Zcash硬分叉,则硬分叉),但这样你的节点操作员和你的社区更普遍地有双重的技术和政治依赖。因此,这样的技术可以用来连接到其他一些链上,但成本会越来越高。基于ZK桥的方案具有吸引人的技术特性,但它们有一个关键的弱点,即它们对51%的攻击或硬分叉不健全。可能有更聪明的解决方案。
理想情况下,我们也希望能保护隐私。如果你有许多钱包是由同一个keystore管理的,那么我们要确保:
这就产生了一些问题:
使用SNARKs,解决方案在概念上很容易:证明默认是信息隐藏的,聚合器需要产生一个递归的SNARK来证明SNARKs。
今天这种方法的主要挑战是,聚合需要聚合器创建一个递归的SNARK,这在目前是相当慢的。
对于KZG,我们可以使用这项关于非索引揭示的KZG证明的工作(另见:Caulk论文中该工作的一个更正式的版本)作为出发点。然而,盲文证明的聚集是一个开放的问题,需要更多的关注。
不幸的是,直接从L2内部读取L1并不能保护隐私,尽管实现直接读取功能仍然是非常有用的,这既是为了最大限度地减少延迟,也是因为它对其他应用的效用。
为了实现跨链的社交恢复钱包,最现实的工作流程是在一个地方维护一个keystore,在许多地方维护钱包,钱包读取keystore是为了(i)更新他们对验证密钥的本地视图,或者(ii) 每个交易过程中进行验证。
使之成为可能的一个关键因素是跨链证明。我们需要努力优化这些证明。无论是ZK-SNARKs,等待Verkle证明,还是定制的KZG解决方案,似乎都是不错的选择。
从长远来看,聚合协议,其中捆绑者产生聚合证明,作为创建所有用户提交的UserOperations的捆绑的一部分,将有必要使成本最小化。这可能应该被整合到ERC-4337生态系统中,尽管可能需要对ERC-4337进行修改。
L2 应该要优化,以最大限度地减少从L2内部读取L1状态(或至少是状态根)的延迟。L2s 直接读取L1状态是理想的,可以节省证明空间。
钱包不仅可以放在 L2 上;你也可以把钱包放在与以太坊连接级别较低的系统上(如 L3,甚至单独的链,只要其包括以太坊状态根,并在以太坊重组或硬分叉时重组或硬分叉)。
然而,keystore应该keystore应该在L1或在高安全性的ZK-rollup L2上。在L1上可以节省大量的复杂性,尽管从长远来看,即使是这样也可能太昂贵了,因此也有需要在L2上建立keystore。
保护的隐私将需要额外的工作,并使一些选项更加困难。然而,我们也许应该朝着保护隐私的解决方案前进,至少要确保我们提出的任何方案都是与保护隐私相兼容的。
特别感谢Yoav Weiss、Dan Finlay、Martin Koppelmann,以及Arbitrum、Optimism、Polygon、Scroll和SoulWallet团队的反馈和审查。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!