理解账户抽象 #4:聚合签名

  • Tiny熊
  • 更新于 2023-03-06 09:36
  • 阅读 2061

通过从头创建智能合约钱包来理解账户抽象,第 4 篇

这里理解账户抽象的第 4 篇,前面几篇为:

  1. 从零开始设计智能合约钱包

  2. 使用Paymaster赞助交易

  3. 使用 create2 创建钱包

  4. (本文)使用 聚合签名

聚合签名

我们目前的实现是分别验证捆绑在一起的每个用户操作。这是一种非常直接的验证方式,但有可能造成浪费。检查签名最终可能会导致昂贵的Gas,因为这样做需要相当多的密码运算。

我们可以用一个签名而不是许多签名来同时验证许多操作,这不是很好吗

要做到这一点取决于密码学的一个概念,聚合签名(aggregate signatures)

一个支持聚合的签名方案提供了一种方法,给定多个用不同密钥签名的信息,生成一个单一的组合签名,这样,验证组合签名就意味着所有的组成签名也是有效的。

支持聚合的签名方案的一个常见例子是BLS

这种优化对实现rollup特别有用,因为rollup的主要目标是数据压缩,而签名聚合让我们压缩签名部分数据大小。

关于签名聚合节省空间的更多信息,请参见Vitalik关于这个问题的推特

聚合器的介绍

马上,我们就会看到,并不是一个捆绑包(bundle)中的所有用户操作都可以将其签名聚合在一起。请记住,一个钱包被允许使用任何想要的逻辑来验证给定的签名,所以在同一个捆绑中可能有各种签名方案。

由于我们可能无法聚合不同方案的签名,我们的捆绑包最终会有几组操作,每组使用不同的聚合方案或根本没有聚合方案。

由于我们需要在链上代表各种聚合方案,每个方案都有自己的逻辑,我们将用一个合约来代表每个聚合方案,我们称之为聚合器

聚合方案是指:如何将多个签名合并成一个签名以及如何验证合并后的签名 ,所以聚合器将这两个功能作为函数公开:

contract Aggregator {
  function aggregateSignatures(UserOperation[] ops)
    returns (bytes aggregatedSignature);

  function validateSignatures(UserOperation[] ops, bytes signature);
}

img

聚合器合约将来自多个用户的操作组合成一个具有单一签名的组。

由于每个钱包都在定义自己的签名方案,因此由每个钱包决定它与哪个聚合器兼容,如果有的话。

如果一个钱包想参与聚合,它得暴露一个方法来选择它的聚合器:

contract Wallet {
  // ...

  function getAggregator() returns (address);
}

使用这个新的getAggregator方法,捆绑者可以将具有相同聚合器的操作分组,并使用该聚合器的aggregateSignatures方法为它们计算一个组合签名。

一个组可能看起来像这样:

struct UserOpsPerAggregator {
  UserOperation[] ops;
  address aggregator;
  bytes combinedSignature;
}

如果一个捆绑者拥有关于特定聚合器的链外知识,它可以通过硬编码签名聚合算法的原生版本进行优化,而不是作为EVM代码运行aggregateSignatures

接下来,我们需要更新入口点合约以利用新的聚合器。

回顾一下,入口点有一个handleOps方法,它接收一个用户操作(OP)列表。

我们将给它一个新的方法,handleAggregatedOps,它做同样的事情,但接受按聚合器分组的操作(Ops):

contract EntryPoint {
  function handleOps(UserOperation[] ops);

    function handleAggregatedOps(UserOpsPerAggregator[] ops);

  // ...
}

这个新方法,handleAggregatedOps,与handleOps的工作原理基本相同。唯一的区别是它的验证步骤。

handleOps通过调用每个钱包的validateOp方法进行验证,而handleAggregatedOps将调用聚合器的validateSignatures方法对每个组的组合签名使用该组的聚合器。

img

执行器(executor)使用聚合器(aggregator)在将用户操作发送到入口点之前将它们组合在一起,因此它们可以同时被验证。

我们几乎完成了!

但这里有一个问题,现在已经很熟悉了。

捆绑者想要模拟验证,并检查聚合器是否会在将一组操作纳入捆绑之前验证这些操作,因为如果验证失败,捆绑者就会被迫支付Gas费。但一个具有任意逻辑的聚合器在模拟过程中很容易成功,但在执行过程中却会失败。

我们将以与paymasters和工厂完全相同的方式来解决这个问题:我们限制聚合器可以访问哪些存储以及可以使用哪些操作码,并要求它在进入点中存入ETH,除非它不访问存储。

这就是聚合签名的内容!

总结

我们在这里所创建的或多或少是ERC-4337的完整架构!在细节上有一些差异,比如一些方法的名称和参数,但没有我认为是架构上的差异。如果我的工作做得很好,你现在应该能够读懂真正的ERC-4337,并理解发生了什么。

补遗与ERC-4337的区别

虽然我们已经有了账户抽象的整体架构,但ERC-4337背后的聪明人想到了一些与我们上述描述略有不同的东西。

让我们来看看其中的一些差别:

1. 验证时间范围

上面,我们文中对钱包的validateOp和paymaster的validatePaymasterOp的返回类型相当模糊。ERC-4337找到了一个很好的方法来利用这个。

钱包非常想做的事情是只允许用户操作在一定时间内有效。否则,一个流氓捆绑者可以在该操作保持很长时间,然后在对捆绑者有利的时候将其纳入捆绑。

钱包可能想通过在验证过程中检查TIMESTAMP来防御这种情况,以确保它不会在太久之后执行,但钱包做不到,因为我们在验证过程中禁止TIMESTAMP来阻止模拟的不准确。这意味着钱包需要另一种方式来表明在什么时候操作是有效的。

因此,ERC-4337给了validateOp一个返回值,钱包可以用它来选择一个时间范围:

contract Wallet {
  function validateOp(UserOperation op, uint256 requiredPayment)
    returns (uint256 sigTimeRange);
  // ...
}

这个返回值表示操作有效的时间范围,是两个挨着的8字节整数。

来自ERC-4337的另一个说明:钱包应该从validateOp返回一个哨兵值,而不是在验证失败的情况下回退,这有助于Gas估计,因为eth_estimateGas不会告诉你在一个回退的交易中使用了多少Gas。

2. 钱包和工厂的任意调用数据

我们的钱包的接口是:

contract Wallet {
  function validateOp(UserOperation op, uint256 requiredPayment);
  function executeOp(UserOperation op);
}

在ERC-4337中,钱包的实际上并没有一个名为executeOp的方法。

相反,用户操作(UserOperation)有一个callData字段:

struct UserOperation {
  // ...
  bytes callData;
}

这将作为调用数据传递给钱包。

对于一个典型的智能合约,这个数据的前四个字节将被解释为一个函数选择器,其余的是函数参数。

这意味着,除了必要的验证Op方法外,钱包可以定义自己的接口,用户操作可以用来调用钱包上的任意方法。

按照同样的思路,在ERC-4337中,工厂合约实际上并没有deployContract方法。他们也接收任意的调用数据,在此案例中,是来自操作的initCode字段。

3. paymaster 和工厂的数据压缩

上面我们说过,用户操作包含的字段可以指定一个付款人,以及要传递给它的数据:

struct UserOperation {
  // ...
  address paymaster;
  bytes paymasterData;
}

在ERC-4337中,这些字段被合并为一个字段,作为一种优化,其中字段的前20个字节是付款人地址,其余的是数据:

struct UserOperation {
  // ...
  bytes paymasterAndData;
}

对于工厂和发送到工厂的数据也是如此:虽然我们使用两个字段factory和factoryData,但ERC-4337将这些字段合并为一个字段initCode

关于账户抽象的分享到这里了, 我们希望你学到了很多关于账户抽象的知识。

点赞 5
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Tiny熊
Tiny熊
0x3aA0...D0A7
登链社区发起人 通过区块链技术让世界变得更好而尽一份力。