Cover photo

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

聚合签名

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

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

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

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

常见的支持聚合签名的方案是 BLS

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

有关签名聚合节省空间的更多信息,请参阅 Vitalik 关于该主题的推文

引入聚合

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

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

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

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

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

  function validateSignatures(UserOperation[] ops, bytes signature);
}
聚合器合约将多个用户的操作合并到一个具有单个签名的组合中
聚合器合约将多个用户的操作合并到一个具有单个签名的组合中

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

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

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方法。

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

我们快完成了!

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

打包者希望模拟验证,并在打包之前验证这些聚合器的签名是合法的,因为如果验证失败,打包者将被迫支付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不会告诉你在回滚的交易中使用了多少gas。

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

我们的钱包接口是:

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

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

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

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

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

对于典型的智能合约,此数据的前四字节将被解释为函数标识符,其余字节将被解释为函数参数。

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

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

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

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

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

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

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

工厂合约也是如此,虽然我们使用两个字段factoryfactoryData,但ERC-4337将它们组合成一个字段initCode

好的,你做到了!

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

更多内容

https://mirror.xyz/0xbitfly.eth/ikaBggQNSvuiotOcdufHFST3Q8eiHujuKWrgeoeLL_0

https://mirror.xyz/0xbitfly.eth/pJGxqKDtogHVdULLhsw8iA0FTl-XbeeXD42ZSdsxdRc

原文链接:

https://www.alchemy.com/blog/account-abstraction-aggregate-signatures