账户抽象的完整指南
关于账户抽象已经有大量文章写作。本文涵盖了你需要了解的一切。它从初学者友好开始,无需预先阅读,然后讨论复杂的主题,比如如何推出自己的签名算法。
账户抽象是一个详细的主题,涵盖了一个术语下的各种概念。重要的是要消除歧义,以获得清晰理解。这是一个引人注目的成就,使开发人员能够为用户设计丰富的功能。我们将从给你高层次的账户抽象概述开始:为什么它出现了。然后我们将讨论技术细节:什么是账户抽象。最后,我们将以一个实际示例结束:如何开始使用账户抽象。现在是喝杯咖啡的好时机!
智能合约账户自以太坊早期就存在。账户抽象释放了智能合约账户的全部潜力。回顾一下智能合约账户是有意义的。
你可能熟悉EOA。每次单击“连接钱包”按钮时,你都会看到这些选项。
RainbowKit 的钱包列表将许多钱包显示为两个独立的组。
如果你想要自主托管,你必须连接到一个 EOA。这个 EOA(比如 Metamask 扩展程序)拥有你的资产。它通过在你的浏览器中存储私钥来实现。你还可以导出你的私钥并在其他地方导入它(在此过程中你的所有资产跟着转移)。Ledger 将私钥存储在硬件设备中,使其更加安全。
但 EOA 并不是唯一可以持有资产的地方。我们知道智能合约也可以持有资产:ETH、ERC20、ERC721 等。除了持有资产,合约还可以执行改变资产状态的功能。
来源:https://dev.to/buildbear/simplifying-account-abstraction-for-the-web3-world-53l5
那么,如果智能合约可以充当用户账户会怎样呢?这就是智能合约账户的理念。然而,它不能充当 EOA,因为每个合约调用都需要由 EOA 发起。直到账户抽象出现之前都是这样。使用账户抽象的钱包可以发起交易。我们稍后会再谈到这一点。
来源:https://metamask.io/news/latest/account-abstraction-past-present-future/
实际上,它们早就已经在做这件事了。看看智能合约账户已经存在了一段时间,但没有能够发起交易的能力。向你的账户添加代码引入了一些有趣的功能。让我们看看一些在账户抽象之前的流行用法。
金库管理
Gnosis Safe 是在以太坊上运行的智能合约钱包,需要至少某数量的人批准交易才能发生(M-of-N)。为什么?DAO 需要多重签名金库管理解决方案,以确保没有一个 DAO 成员可以独自移动资金。
观看从第 2 分 42 秒开始的产品演示2 分钟,看看 Gnosis Safe 的实际操作。
社交恢复
Argent 钱包提供了一种无需助记词即可恢复钱包的方法。Argent 的 Guardians 计划允许用户指定可信用户,并决定谁可以恢复信息。Guardians 需要对你的钱包信息进行多数授权才能返回。
观看从第 2 分 21 秒开始的产品演示,看看 Argent 钱包的实际操作。
流支付
Sablier 是一个代币流式传输协议,可以在 web3 中实现按秒支付。DAO 和企业用于解锁、工资等。它们不被视为智能合约账户。但它展示了智能合约账户可以支持的各种功能。
好吧,我知道你可能有点烦了,因为你已经读了这么多内容。而我们还没有真正谈论账户抽象!我们现在开始进入这个话题。我保证这篇文章值得一读。
回到这个想法,智能合约账户最终可以取代 EOA。EOA 存在一些问题。
EOA 钱包体验确实可能是一场噩梦。数一数这里需要借款 Arbitrum 网络上的资产所需的步骤数量。
如果这一切能简化,我们就可以将深爱的 web2 登录体验带到 web3。这正是账户抽象所提供的。有一些不同的实现,比如 Web3Auth 提供的一个实现。这里最大的问题是你无法轻松地在其他地方访问这个钱包。用户会得到特定于应用程序的钱包。因此,虽然你在新用户引导方面取得了胜利,但你完全忽略了 web3 的要点。
Argent 正在构建一个在 StarkNet 上部署的账户抽象的智能合约账户,该账户使用用户名和密码。StarkNet 为账户抽象提供了更原生的支持。最终结果是,相同的用户名和密码可以在 StarkNet 上的不同应用程序中使用,让你访问相同的账户。在这里观看产品演示:
另一种方法是通过 WebAuthn。你可以使用智能手机进行身份验证,而不是记住密码,并且密钥存储在智能手机的安全区域内。Goldfinch Finance 的这个演示非常酷:https://defifortheworld.com/。此外,Rhinestone 正在进行一些前沿工作:https://www.rhinestone.wtf/demo。
可以说,账户抽象注定会给去中心化应用的用户体验带来巨大冲击。尚不清楚用户是否会采用使用安全区域或使用用户名和密码的方法,甚至是否会选择特定于应用程序的账户。
如果你以前使用过去中心化应用程序,你会知道当你打算执行某个操作时,必须进行多次调用,等待它们执行,并且每次都要支付 gas 费用,这是多么令人讨厌。通过账户抽象,你可以在一个原子交易中执行一系列操作。这个功能被称为multicall。
来源链接
这是一个非常庞大的话题,仅凭一篇文章无法涵盖。如果读者感兴趣,我建议他们阅读更多关于基于意图的架构的内容。意图超越了账户抽象。以下是意图的简短描述:
如果一个交易明确地涉及“如何”执行一个操作,那么意图涉及“期望该操作的预期结果是什么”。如果一个交易说“执行 A 然后 B,支付确切的 C 以获得 X”,那么意图说“我想要 X,我愿意支付最多 C”。
当我们讨论账户抽象的工作原理时,我们会回到这个话题。
账户抽象增加了一个额外的功能。即让第三方(称为支付主 paymaster )有能力代表用户支付 gas 费用。
来源:https://usa.visa.com/solutions/crypto/rethink-digital-transactions-with-account-abstraction.html
或者让用户使用 USDC 等 ERC20 代币支付费用。
来源:https://usa.visa.com/solutions/crypto/rethink-digital-transactions-with-account-abstraction.html
你可以设置规则,比如新用户在你的平台上的前 10 笔交易无需支付 gas 费用。有了这个功能,用户可以使用信用卡在去中心化应用程序上购买 NFT。并且可以与之交互,而无需事先给他们的钱包充值。这是一个重大的用户体验改进。这可以以多种方式实现。
还记得我们之前提到过智能合约账户就像 EOA 一样,但具有代码,以及智能合约账户可以设计成以启用各种用例,比如金库管理、社交恢复、流支付等。对启用了账户抽象的智能合约账户也是一样的。通过改进的用户体验,所有这些早期的用例甚至可以更加动态。事实上,正在进行一项提案,引入了一种新颖的概念,即模块化账户抽象。这超出了本文的范围。但我们将在未来的博客文章中回到这个话题!
接下来的部分将专注于技术。不过,我要提个警告。如果你对区块链不熟悉,我建议你在这里暂停一下,先了解一些基础知识。
既然你已经看到了账户抽象的最终结果,让我们试着重新设计它。准确地说,我们正在设计一个解决方案,用户无需连接 Metamask 等外部钱包。非托管钱包在他们打开去中心化应用程序时就可用,并且他们也可以在其他地方访问。他们能够存储所有的资产,并且通过引入可编程性,钱包的功能更加灵活。此外,我们还希望所有现有的工具能够正常工作,比如 Etherscan。
此外,我们将评估钱包的设计是否能够给我们提供以下属性:
立即浮现出的问题是,应用程序如何知道是 Alice 访问了该应用程序。我们如何确保是 Alice 授权了从她的账户中移动资产的操作?
👉 注意:严格来说,授权是不正确的术语,因为它类似于访问控制。但为了简单起见,我们将忽略传统的定义。
EOA 与一个称为签名者(signer)的加密对象相关联。
签名者,也称为密钥对,由两个密钥组成:私钥和公钥。
私钥可用于对数字消息进行签名,公钥可以让任何人验证给定签名是由其对应的私钥签名的。EOA 的地址是从签名者的公钥派生的。
此外,为了防止重放攻击并保持顺序,执行交易之前,EVM 将验证交易nonce是否与账户 nonce 匹配。
答案很简单:它需要解耦。我们需要将签名者存储在其他地方。所以现在账户由代码(和存储)、资产和一个 nonce 组成。
在这一点上,我们已经为理想的设置设计了高级别的设计。现在我们需要将其回溯到以太坊。签名者(signer)可以存储在设备上。因此,我们不需要为技术细节烦恼。但我们如何在以太坊网络上实现这一点呢?这就是困难的部分。希望你已经准备好了。
鉴于上述要求,我们采取以下一种或多种方法:
不对协议进行任何更改,使现有基础设施正常工作?或 使 EOA 可编程?或 林的 废弃 EOA 并允许合约账户接受签名?
我们可以采用的最简单的方法是仅将 EOA 用于签名目的。如果我们仅将 EOA 用于签名目的,则私钥可以简单地存储在设备安全区域(enclave)中,并且无需安装外部钱包。用户像往常一样使用他们的EOA(现在存储在设备上)进行身份验证,并批准代表他们采取的某些操作。这被发送到一个 gas 中继器,该中继器支付 gas 费(需要以某种方式获得资金),并将交易放在链上。受信任的转发器可以提取有关原始发送方的信息,并将该信息发送给需要与之交互的合约。这种方法被称为元交易(meta-transactions),已经被许多合约采用。然而,它有一些主要的缺点。
积极的一面是,用户无需进行任何更改,一切都应该正常工作。也不需要硬分叉。有一些成熟的系统,比如OpenGSN,可以充当中继器。
然而,挑战在于现在所有的合约都需要符合这些标准。全局变量msg.sender
不能用于识别交易签名者——而msg.sender
在许多地方都被使用。相反,它们需要遵守EIP-2771以检索有关用户的信息。此外,中继器可以要求其自定义实现消息格式,并且合约必须适应中继器标准。这也导致了中继器层面的中心化。此外,gas 成本可能相当高。
EIP-3074提议采取了这种方法。这本质上是一种原生方法,超越了元交易。它们引入了新的 EVM 操作码 AUTH 和 AUTHCALL,赋予称为调用者( invokers) 的账户特殊权限。
用户使用他们的私钥对包含其意图的消息进行签名。然后,该消息被包含在链上的交易中,调用一个调用者(合约)。调用者使用签名的消息以及 AUTH 操作码来控制用户的帐户,并使用 AUTHCALL 执行用户执行操作。
积极的一面是,它可以使用 ERC20 代币支付 gas。但不利的一面是,它需要进行硬分叉,因为这是与以太坊区块链当前运行方式的重大改变。另一个问题是调用者拥有过多的权力,这会带来安全风险。然而,一个新的 L2 可以从一开始就引入这一点。
然而,这种方法本身不支持可编程性。另一个提案EIP-5003在此方法的基础上,赋予 EOA 完整的智能合约功能。然而,考虑到 EIP-3074 的现有挑战,我们现在不需要进一步阐述 EIP-5003。
在第一种方法中,用户向中继器发送了一个特殊的签名消息。第二种方法改进了第一种方法,并添加了一些 EVM 逻辑,使其更加无缝。我们即将讨论的第三种方法使得合约能够按照用户的指示执行任意操作。这就是EIP-4337的内容。它比方法 1 和 2 要复杂得多,这两种方法更加直观。EIP-4337 可以分为两个流程:在合约层发起交易和在交易层包含用户操作。
让我们分开讨论它们,从发起交易的问题开始。如何让智能合约发起交易成为可能,特别是因为我们知道 EIP-4337 没有引入任何新的操作码,也不需要进行硬分叉。
简单的答案是引入一个名为捆绑器(bundler)的 EOA 代理。让我们想象一下,捆绑器包含一堆需要执行的用户操作。等等,什么是用户操作。暂时来说,你可以把它想象成一种交易请求。如果你对这个想法有所了解,你可能会说它与元交易提案中的受信任转发器有些相似。有点。但不需要一个合约需要实现 ERC-2771 接口,我们有一个入口点合约,它调用用户的智能合约钱包的方法来验证操作并检查其签名;执行实际操作;并支付 gas。你可以把UserOperation
看作是在通常交易上创建的一种抽象。此外,这里也有支付主(paymaster)的角色。但为了简洁起见,我们暂时跳过它。
struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}
一个钱包是一个智能合约,并且需要有两个功能:
validateOp
验证签名和 nonce,支付费用并在验证成功时增加 nonce,如果验证失败则抛出异常。executeOp
将 calldata 解释为钱包执行操作的指令。好了,现在我们知道了操作是如何验证和执行的。让我们来谈谈捆绑器(bundler)如何将这些操作上链。
谁支付 gas 费用?捆绑器以 ETH 支付捆绑交易的费用,并通过所有 UserOperation 执行支付部分费用来获得补偿。捆绑器根据类似于矿工在现有交易内存池中费用优先逻辑,选择包括哪些 UserOperation 对象。这就是为什么我们有一个另类内存池,它是一个无需许可的 P2P 网络。此外,捆绑器可以聚合操作,因此诸如签名验证之类的操作不需要多次执行。
这有点复杂!但让我们来谈谈这种设计的优缺点。显而易见的是,无需硬分叉,可以直接包含所有更改。这里的另一个优点是,生态系统中存在的智能合约钱包应该能够正常运行。不利之处是,第一次部署钱包时,可能会产生高昂的 gas 费用。还有一些其他需要牢记的考虑。但是,就目前而言,这已经足够了,因为我认为我们可能已经找到了胜利者。
从上表可以看出,哪种方法更好应该是显而易见的。更好的消息是,账户抽象(因为它只是代码)现在已经在以太坊上实现。你可以在这里浏览实现: https://github.com/eth-infinitism/account-abstraction
我试图使本节内容直观易懂。如果你有兴趣了解更多关于协议的内容,我鼓励你查看下面的推荐阅读部分
首先,那太棒了。我真的相信这个领域充满了机遇。有了正确的想法,你可以构建一个可以被数百万用户使用的应用程序或工具。让我们总结一下账户抽象的全部范围。
这里有很多组件。我们有入口点合约和所有必要的逻辑来实现账户抽象。但除此之外,开发人员可以专注于许多领域:
好消息是,所有这些组件已经以某种形式存在。例如,如果开发人员想要专注于纯粹处理密钥并确保良好的用户体验,那么他们无需担心系统的其余部分。同样,也已经准备好了可使用的支付主(paymaster)。
我们已经定义了UserOperation
。现在让我们来解码它:
initCode
。了解这些字段肯定很好的,但好消息是有一些库会默认它们的一些值,所以对于大多数情况,你无需担心。其中一个库是userop.js,我们将使用它。这个库创建一个 UserOperation 对象,并将其传递给捆绑器客户端,然后转发到一个内存池,捆绑器将从内存池获取UserOperation。
这里的最后一块拼图是找出要发送给哪个捆绑器。我们将使用StackUp。但市场上有各种捆绑器。以下是一个完整的代码片段允许你来发送。你所需要做的就是提供一个SIGNING_KEY
(你可以从现有钱包中提取,或者生成一个新的,当你执行代码时它将部署)和RPC_URL
(你可以从 StackUp 获取)。
import * as dotenv from "dotenv";
import { ethers } from "ethers";
import { Presets, Client } from "userop";
dotenv.config();
const signingKey = process.env.SIGNING_KEY || "";
const rpcUrl = process.env.RPC_URL || "";
console.log(rpcUrl);
async function approveAndSend(
token: string,
to: string,
value: string
): Promise<any[]> {
const ERC20_ABI = require("./erc20.abi.json");
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const erc20 = new ethers.Contract(token, ERC20_ABI, provider);
const decimals = await Promise.all([erc20.decimals()]);
const amount = ethers.utils.parseUnits(value, decimals);
const approve = {
to: token,
value: ethers.constants.Zero,
data: erc20.interface.encodeFunctionData("approve", [to, amount]),
};
const send = {
to: token,
value: ethers.constants.Zero,
data: erc20.interface.encodeFunctionData("transfer", [to, amount]),
};
return [approve, send];
}
async function main() {
// Create a userop builder
const signer = new ethers.Wallet(signingKey);
const builder = await Presets.Builder.Kernel.init(signer, rpcUrl);
const address = builder.getSender();
console.log("address: ", address);
// Create the calls
const to = address;
const token = "0x7af963cf6d228e564e2a0aa0ddbf06210b38615d";
const value = "0";
const calls = await approveAndSend(token, to, value);
builder.executeBatch(calls);
console.log(builder.getOp());
// Send the user operation
const client = await Client.init(rpcUrl);
const res = await client.sendUserOperation(builder, {
onBuild: (op) => console.log("Signed UserOperation: ", op),
});
console.log(`UserOpHash: ${res.userOpHash}`);
console.log("Waiting for transaction...");
const ev = await res.wait();
console.log(`Transaction hash: ${ev?.transactionHash ?? null}`);
}
export default main();
上面的代码创建了一个批量用户操作,用于授权和转移 ERC20 代币,并将其转换为批量操作。上面的示例中的代币地址是部署在 Goerli 网络上的一个测试代币。此外,erc20.abi.json
是一个标准的 ERC20 ABI JSON。
我使用了一个由我生成的签名密钥来运行上面的示例,你可以在这里找到它:https://goerli.etherscan.io/address/0xd7F9818f6D9Fd9091D39D84d6359a1d6205F53A3。而交易可以在这里找到它:https://goerli.etherscan.io/tx/0x3dc7ec956db972a1f80dd5cc1fb6e8bec79bf8b36cb9d836426d272939a42200。注意交易显示它来自入口点合约。这是符合预期的。
有了上面的片段和一种巧妙的获取签名密钥的方法,就可以创建一个非常流畅的用户体验。这个库的好处是,它与ethers
库解耦的,特别是对于调用完全新的用户流程,使得编写全新的用户流程更加简单。
所以,现在我们已经看到了用户操作是如何构建的,以及可能提供了一个非常流畅的用户体验——假设相当简单地传递某种签名密钥,让我们把注意力转向另一件事。就我个人而言,我认为这是真正的魔力所在。我们如何构建一个全新的钱包体验?
首先,值得指出的是,我们在上面的示例中也使用了智能合约钱包。它只是对用户进行了混淆。它是在Presets.Builder.Kernel.init
函数中初始化的。ZeroDev允许我们相当容易地定制我们使用的钱包。他们通过让你插入任何验证器(validator)提供者来实现这一点。这个验证器提供者用于验证签名。以下是一个使用ECDSAProvider
的片段,允许你铸造 NFT。
import * as dotenv from "dotenv";
dotenv.config();
import { ECDSAProvider } from "@zerodev/sdk";
import { LocalAccountSigner, UserOperationCallData } from "@alchemy/aa-core";
import { JsonRpcProvider, Contract } from "ethers";
import contractABI from "./../assets/erc721.abi.json" assert { type: "json" };
export type Hex = `0x${string}`;
// ZeroDev Project ID
const projectId = process.env.PROJECT_ID as string;
const privateKey = process.env.PRIVATE_KEY as Hex;
const AlchemyHttps = process.env.ALCHEMY_HTTPS;
// The "owner" of the AA wallet, which in this case is a private key
const owner = LocalAccountSigner.privateKeyToAccountSigner(privateKey);
console.log(await owner.getAddress());
// The NFT contract we will be interacting with
const contractAddress = "0x932ca55b9ef0b3094e8fa82435b3b4c50d713043";
const provider = new JsonRpcProvider(AlchemyHttps);
const erc721 = new Contract(contractAddress, contractABI, provider);
const main = async () => {
// Create the AA wallet
const ecdsaProvider = await ECDSAProvider.init({
projectId,
owner,
usePaymaster: true,
opts: {
paymasterConfig: {
policy: "VERIFYING_PAYMASTER",
},
},
});
const address = await ecdsaProvider.getAddress();
console.log("My address:", address);
// Mint the NFT
const { hash } = await ecdsaProvider.sendUserOperation({
target: contractAddress,
data: erc721.interface.encodeFunctionData("mint", [1n]) as Hex,
});
await ecdsaProvider.waitForUserOperationTransaction(hash as Hex);
// Check how many NFTs we have
const balanceOf = await erc721.balanceOf(address);
console.log(`NFT balance: ${balanceOf}`);
// Mint multiple NFTs
const userOp = {
target: contractAddress,
data: erc721.interface.encodeFunctionData("mint", [1n]) as Hex,
} as UserOperationCallData;
const batch = await ecdsaProvider.sendUserOperation([userOp, userOp]);
await ecdsaProvider.waitForUserOperationTransaction(batch.hash as Hex);
// Check how many NFTs we have
const balanceOf2 = await erc721.balanceOf(address);
console.log(`NFT balance: ${balanceOf2}`);
};
main().then(() => process.exit(0));
你可以用 ZeroDev 今天提供的任何验证器提供者中的任何一个替换这个提供者。其中一些提供了社交登录,其他的是 JWT。这是一个正在进行中的工作,预计会在这方面看到一些进展。
实际上,如果你看上面的片段,它已经具有支付主功能。StackUp 和 ZeroDev 都允许你通过中间件轻松地添加支付主功能。在 ZeroDev 中,你可以设置策略来控制你要支付的交易的种类和数量。
如果你想使用 ERC20 代币为用户支付,可以使用以下片段
const ecdsaProvider = await ECDSAProvider.init({
projectId, // zeroDev projectId
owner,
opts: {
paymasterConfig: {
policy: "TOKEN_PAYMASTER",
gasToken: "USDC",
},
},
})
另外,Visa 也在尝试这个想法。
我们已经广泛涵盖了用户操作和验证者,但我们还没有谈论捆绑器(bundlers)。我之所以没有跳过这些内容,是因为本指南的目的是让用户构建钱包并创造令人惊叹的用户体验。然而,bundler 是 AA 基础设施的核心部分,值得单独讨论。大多数开发人员不会想要推出自己的捆绑器,除非他们强烈感到有必要。StackUp 的 bundler 应该满足大多数用例。还有其他一些你可以探索。我们不会讨论捆绑器,但你可以自由地深入研究。
有几件事情需要注意。我们正处于采用的早期阶段。账户抽象已经部署在主网和一些 L2 上。像 Safe 这样的智能合约钱包已经采用了这项技术,但还有许多应用程序没有采用。我相信随着工具的健壮性和更多的参与者开始采用,情况会发生变化。Kofi建立了一个Dune Dashboard来展示 EIP-4337 的采用情况。
另一件需要记住的事情是,账户抽象是作为高级抽象构建的。但像 zkSync 和 StarkNet 这样的 L2 已经在协议层面采用了它。这意味着它在这些链上得到了更原生的支持。相当多的专家相信,最终原生的账户抽象也将出现在以太坊上。
来自链接
好的,首先 MPC (Multi-Party Computation)是什么?多方计算是一种技术,用于在多方拥有的私有数据上安全计算函数,而不将这些数据透露给其他方。这被称为分布式密钥生成和签名。
这种技术确实允许你在丢失密钥时恢复密钥,并具有一些其他好处。但它并不真正与账户抽象竞争——账户抽象还提供了许多其他优势。一个思考的方式是,账户抽象涉及的是发生在链上的一切。而 MPC 只涉及密钥管理。在这方面,它们不仅不相互竞争,实际上它们的方法有些正交。
如果你喜欢阅读本文。请在 Twitter 上关注我 @messagehash!私信也是开放的。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!