# 账户抽象之路#4-聚合签名

By [0xBitFly](https://paragraph.com/@0xbitfly) · 2023-04-03

---

聚合签名
----

我们当前的实现方案，是分别验证捆绑包中的每个用户操作，这是一种非常直接的验证方式，但会造成gas的浪费，检查签名最终可能会在 gas-wise 方面变得昂贵，因为这样做需要运行相当多的加密算法运算。

如果我们可以只用一个签名，而不是多个签名同时验证许多操作，那不是很好吗？

这样做取决于密码学中的一个概念，聚合签名。

支持聚合的签名方案提供了一种方法，给定多个使用不同密钥签名的消息，然后生成单个组合签名，验证这个组合签名，如果组合签名验证通过，那么下面所有的单个签名也都是合法的。（译注：有点类似于merkle tree，减少链上存储体积，这里是减少签名验证步骤）

常见的支持聚合签名的方案是 [BLS](https://en.wikipedia.org/wiki/BLS_digital_signature)。

这种优化对于实现卷叠(Rollup)特别有用，因为rollup的主要目的是数据压缩，而签名聚合让我们可以压缩签名部分。

有关签名聚合节省空间的更多信息，请参阅 Vitalik 关于该主题的[推文](https://twitter.com/VitalikButerin/status/1554983955182809088)。

引入聚合
----

我们立即看到，并非捆绑包中的所有用户操作都可以将其签名汇总在一起。请记住，钱包被允许使用它想要的任何逻辑来验证其给定的签名，因此同一捆绑包中可能存在各种签名方案。

由于我们可能无法聚合来自不同方案的签名，我们的捆绑包最终将产生N组操作，每个组使用不同的聚合方案或根本没有聚合方案。

由于我们需要在链上表示各种聚合方案，每个方案都有自己的逻辑，因此我们将让每个聚合方案用一个合约表示，我们将其称为**聚合器**。

一个聚合方案的定义是它如何将多个签名组合成一个，以及如何验证组合签名，因此聚合器有以下两个接口：

    contract Aggregator {
      function aggregateSignatures(UserOperation[] ops)
        returns (bytes aggregatedSignature);
    
      function validateSignatures(UserOperation[] ops, bytes signature);
    }
    

![聚合器合约将多个用户的操作合并到一个具有单个签名的组合中](https://storage.googleapis.com/papyrus_images/4518f1ff2441f021fe89e1d7adaf4c10df46fbbc5fef985d72f9cb1f92a99d7c.jpg)

聚合器合约将多个用户的操作合并到一个具有单个签名的组合中

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

如果钱包想要参与聚合，它会提供一种获取其聚合器的方法

    contract Wallet {
      // ...
    
      function getAggregator() returns (address);
    }
    

使用这种新的getAggregator方法，打包者可以将具有相同聚合器的操作分组在一起，并使用该聚合器的aggregatorSignatures方法为它们计算组合签名。

一个组合看起来像这样：

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

> 如果打包者具有某一特定聚合器的链下知识（译注：源码？），则可以通过硬编码本机版本的签名聚合算法，来优化这一操作，而不是运行aggregateSignatures的EVM代码。

接下来，我们需要更新入口点合约，才能使用新的聚合器。

回想一下，入口点有一个`handleOps`方法，它接收一个ops的列表作为输入参数。

我们将给它一个新方法，`handleAggregatedOps`，它做同样的事情，但接受的是按聚合器分组后的操作：

    contract EntryPoint {
      function handleOps(UserOperation[] ops);
    
        function handleAggregatedOps(UserOpsPerAggregator[] ops);
    
      // ...
    }
    

新方法`handleAggregatedOps`的工作原理与`handleOps`基本相同。唯一的区别在于它的验证步骤。

虽然`handleOps`通过调用每个钱包的`validateOp`方法来执行验证，但`handleAggregatedOps`将使用聚合器在每个组的组合签名上 调用聚合器的`validateSignatures`方法。

![执行者使用聚合器将用户操作分组，然后再将它们发送到入口点，因此它们都可以同时进行验证](https://storage.googleapis.com/papyrus_images/189bd257e2be4255fa3997ab1bd0f769fa868f72b53a37685305cce8bc69a815.jpg)

执行者使用聚合器将用户操作分组，然后再将它们发送到入口点，因此它们都可以同时进行验证

我们快完成了！

但这里有一个已经很熟悉的问题。

打包者希望模拟验证，并在打包之前验证这些聚合器的签名是合法的，因为如果验证失败，打包者将被迫支付gas费用。但具有任意逻辑的聚合器在模拟过程中很容易成功，但在执行过程中失败。

我们用之前代付者和工厂合约完全相同的方式解决这个问题：我们会限制聚合器可以访问哪些存储以及它可以使用哪些操作码，并要求它在入口点合约质押ETH，除非它不访问存储。

这就是聚合签名！

热身
--

我们在这里创建账户抽象是与ERC-4337的完整架构近乎一样的！只是细节上有一些差异，例如一些方法的名称和参数，但架构上几乎没什么大的差异了。如果我这篇文章解释的很好，你现在应该能够理解ERC-4337具体是什么样了。

如果你已经读到了这里了，非常感谢你阅读我这一版本的解释！我希望它能帮助你，就像它能帮助我一样。

附录：与ERC-4337的差异
---------------

虽然我们已经了解了帐户抽象的整体架构，但ERC-4337背后的聪明人想到了一些与我们上面描述略有不同的事情。

让我们来看看其中的一些！

### 1\. 验证时间范围

上图，我对钱包的`validateOp`和代付者的`validatePaymasterOp`的返回类型非常困惑。ERC-4337找到了利用这一点的好方法。

钱包非常想做的事情是，只允许用户操作在一定时间内有效。否则，恶意的打包者可以让用户操作停留很长时间，然后在更长时间后将其包含在对打包者有利的捆绑包中。

钱包可以通过在验证期间检查TIMESTAMP来防止这种情况，以确保这个操作不是停留了太长时间，但它行不通，因为我们在验证期间禁止了TIMESTAMP，以防止线下模拟不准确的情况，这意味着钱包需要另一种方式来指示操作在什么时间有效。

因此，ERC-4337给了`validateOp`一个返回值，钱包可以使用该值来选择有效的时间范围：

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

此返回值表示验证有效的时间范围，用两个8字节整数表示。

ERC-4337的另一个注意事项：在验证失败的情况下，钱包应该从`validateOp`返回标识值（sentinel value），而不是回滚，这有助于估算gas费，因为[eth\_estimateGas](https://docs.alchemy.com/reference/eth-estimategas)不会告诉你在回滚的交易中使用了多少gas。

### 2\. 钱包合约和工厂合约任意数据调用

我们的钱包接口是：

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

在ERC-4337中，智能钱包实际上没有名为`executeOp`的方法。

相反，用户操作有一个`callData`字段：

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

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

对于典型的智能合约，此数据的前四字节将被解释为[函数标识符](https://zhuanlan.zhihu.com/p/421087023)，其余字节将被解释为函数参数。

这意味着除了所需的`validateOp`方法外，钱包可以定义自己的接口，并且用户操作可用于调用钱包上的任意方法。

同样，在ERC-4337中，工厂合约实际上没有`deployContract`的方法，他们也接收任意的call数据，在这种情况下，是从操作的`initCode`字段接收。

### 3\. 代付者和工厂合约的压缩数据

上面我们说过，用户操作包含指定代付者的字段，以及要传递给它的数据：

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

在ERC-4337中，这些被合并到一个字段中作为优化，其中字段的前20个字节是代付者地址，其余是数据：

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

工厂合约也是如此，虽然我们使用两个字段`factory`和`factoryData`，但ERC-4337将它们组合成一个字段`initCode`。

好的，你做到了！

我们希望您学到了很多关于帐户抽象的知识。

更多内容
----

[https://mirror.xyz/0xbitfly.eth/ikaBggQNSvuiotOcdufHFST3Q8eiHujuKWrgeoeLL\_0](https://mirror.xyz/0xbitfly.eth/ikaBggQNSvuiotOcdufHFST3Q8eiHujuKWrgeoeLL_0)

[https://mirror.xyz/0xbitfly.eth/pJGxqKDtogHVdULLhsw8iA0FTl-XbeeXD42ZSdsxdRc](https://mirror.xyz/0xbitfly.eth/pJGxqKDtogHVdULLhsw8iA0FTl-XbeeXD42ZSdsxdRc)

**原文链接：**

[https://www.alchemy.com/blog/account-abstraction-aggregate-signatures](https://www.alchemy.com/blog/account-abstraction-aggregate-signatures)

---

*Originally published on [0xBitFly](https://paragraph.com/@0xbitfly/4)*
