我们当前的实现方案,是分别验证捆绑包中的每个用户操作,这是一种非常直接的验证方式,但会造成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背后的聪明人想到了一些与我们上面描述略有不同的事情。
让我们来看看其中的一些!
上图,我对钱包的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。
我们的钱包接口是:
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字段接收。
上面我们说过,用户操作包含指定代付者的字段,以及要传递给它的数据:
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/pJGxqKDtogHVdULLhsw8iA0FTl-XbeeXD42ZSdsxdRc
原文链接:
https://www.alchemy.com/blog/account-abstraction-aggregate-signatures

