通过从头创建智能合约钱包来理解账户抽象,第 4 篇
这里理解账户抽象的第 4 篇,前面几篇为:
我们目前的实现是分别验证捆绑在一起的每个用户操作。这是一种非常直接的验证方式,但有可能造成浪费。检查签名最终可能会导致昂贵的Gas,因为这样做需要相当多的密码运算。
我们可以用一个签名而不是许多签名来同时验证许多操作,这不是很好吗?
要做到这一点取决于密码学的一个概念,聚合签名(aggregate signatures)。
一个支持聚合的签名方案提供了一种方法,给定多个用不同密钥签名的信息,生成一个单一的组合签名,这样,验证组合签名就意味着所有的组成签名也是有效的。
支持聚合的签名方案的一个常见例子是BLS。
这种优化对实现rollup特别有用,因为rollup的主要目标是数据压缩,而签名聚合让我们压缩签名部分数据大小。
关于签名聚合节省空间的更多信息,请参见Vitalik关于这个问题的推特。
马上,我们就会看到,并不是一个捆绑包(bundle)中的所有用户操作都可以将其签名聚合在一起。请记住,一个钱包被允许使用任何想要的逻辑来验证给定的签名,所以在同一个捆绑中可能有各种签名方案。
由于我们可能无法聚合不同方案的签名,我们的捆绑包最终会有几组操作,每组使用不同的聚合方案或根本没有聚合方案。
由于我们需要在链上代表各种聚合方案,每个方案都有自己的逻辑,我们将用一个合约来代表每个聚合方案,我们称之为聚合器。
聚合方案是指:如何将多个签名合并成一个签名以及如何验证合并后的签名 ,所以聚合器将这两个功能作为函数公开:
contract Aggregator {
function aggregateSignatures(UserOperation[] ops)
returns (bytes aggregatedSignature);
function validateSignatures(UserOperation[] ops, bytes signature);
}
聚合器合约将来自多个用户的操作组合成一个具有单一签名的组。
由于每个钱包都在定义自己的签名方案,因此由每个钱包决定它与哪个聚合器兼容,如果有的话。
如果一个钱包想参与聚合,它得暴露一个方法来选择它的聚合器:
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
方法对每个组的组合签名使用该组的聚合器。
执行器(executor)使用聚合器(aggregator)在将用户操作发送到入口点之前将它们组合在一起,因此它们可以同时被验证。
我们几乎完成了!
但这里有一个问题,现在已经很熟悉了。
捆绑者想要模拟验证,并检查聚合器是否会在将一组操作纳入捆绑之前验证这些操作,因为如果验证失败,捆绑者就会被迫支付Gas费。但一个具有任意逻辑的聚合器在模拟过程中很容易成功,但在执行过程中却会失败。
我们将以与paymasters和工厂完全相同的方式来解决这个问题:我们限制聚合器可以访问哪些存储以及可以使用哪些操作码,并要求它在进入点中存入ETH,除非它不访问存储。
这就是聚合签名的内容!
我们在这里所创建的或多或少是ERC-4337的完整架构!在细节上有一些差异,比如一些方法的名称和参数,但没有我认为是架构上的差异。如果我的工作做得很好,你现在应该能够读懂真正的ERC-4337,并理解发生了什么。
虽然我们已经有了账户抽象的整体架构,但ERC-4337背后的聪明人想到了一些与我们上述描述略有不同的东西。
让我们来看看其中的一些差别:
上面,我们文中对钱包的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。
我们的钱包的接口是:
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
字段。
上面我们说过,用户操作包含的字段可以指定一个付款人,以及要传递给它的数据:
struct UserOperation {
// ...
address paymaster;
bytes paymasterData;
}
在ERC-4337中,这些字段被合并为一个字段,作为一种优化,其中字段的前20个字节是付款人地址,其余的是数据:
struct UserOperation {
// ...
bytes paymasterAndData;
}
对于工厂和发送到工厂的数据也是如此:虽然我们使用两个字段factory和factoryData,但ERC-4337将这些字段合并为一个字段initCode
。
关于账户抽象的分享到这里了, 我们希望你学到了很多关于账户抽象的知识。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!