最近电报自动交易机器人和各种 SocialFi 很火,这些产品给用户带来了类似 Web2 的良好用户体验。但火爆的背后,也发生了多起安全事件。为此,很多新上线的平台开始使用更先进的账户安全技术来保护用户资产,比如@tomo_social
使用了ERC-4337
账户抽象技术,有些电报机器人采用了MPC 钱包
技术。
尽管账户抽象钱包(AA 钱包)已经具备了零 gas 费(服务商代付 gas 费),多签,社交登录等强大功能,并大幅度提升用户体验,但是因为ERC-4337
属于在现有以太坊共识基础上的补丁方案,与链交互签名时仍旧需要私钥,各种方案只是在私钥保存和签名环节采取各种安全措施。
所以,虽然很多代用户签名交互的电报机器人,SocialFi 平台通过MPC 钱包
或AA 钱包
来保障客户的私钥安全,但实际上,因为最终还是要通过钱包主私钥来进行签名,本质上还是私钥的验证模式,所以仍旧有私钥泄露的风险。
今天看到AA 钱包
创新项目ZeroDev
的Session Key( 对话密钥 )
解决方案,可以让 AA 钱包授权生成一个或若干个Session Key
(也是一种私钥),来受控的执行经授权的操作。这种授权模式有别于ERC-20
或ERC-721
的合约资产的使用额度授权,更有别于私钥的验证模式,一旦授权Session Key
后,可以通过观察者(如服务器)自动执行授权范围内的合约或链上交互操作,从而可以带来更安全更便捷的用户体验。
下面内容参考:https://docs.zerodev.app/use-wallets/use-session-keys
会话密钥(Session keys)是ZeroDev
的AA 钱包
中最强大的功能之一,它具有许多很多实际的应用场景,能解决很多痛点。
传统的EOA 钱包
只有一个用来为钱包签署交易的密钥(私钥)。如果拥有该密钥,就拥有了该钱包。这就是为什么绝对不能丢失或泄露助记词(seed phrase)或私钥的原因。
然而,使用AA 钱包
时,钱包与密钥是分离的,即:钱包所有者可以为任意密钥指定发起交易的权限,并随时撤销这些密钥的访问权限,甚至还可以限定密钥的范围,使其只能在特定条件和特定时间窗口内发送有限的交易。
我们称这些受到主密钥控制的密钥为会话密钥。
有一篇ZeroDev
创始人写的文章可以参考:《会话密钥是 Web3 的 JWT》(Session Keys are the JWTs of Web3)
Session Key
有几个核心用途:
如果您正在构建一个高频交互的 Dapp,可能希望用户不必频繁的手动确认每个交易。这时,可以为用户的当前“会话”创建一个Session Key
,并限定该Key
的使用时间和范围,使其只能发送您所允许的交易,并且该Key
在当前会话结束后失效。设定后,可以使用该Session Key
与 Dapp 进行交互,而无需使用他们的主密钥对每一个交易都进行手动确认。
通常情况下,交易需要由钱包所有者主动发起。然而,有时候“自动化”交易能实现更佳的用户体验(如电报机器人等)。例如,如果您正在构建一个电报交易 bot,您可能希望为用户提供一个功能,即当价格接近设定的目标价格时,自动执行买入或卖出的交易。在这种情况下,可以创建一个Session Key
,仅当价格确实接近目标价时才允许执行。把这个 bot 部署到一个“观察者”(比如服务器)共享会话密钥,当条件发生时,观察者(服务器)会为用户自动执行交易。
这类应用场景非常大,不仅仅包括自动交易机器人实现的挂单交易,跟随交易,自动抢单,还包括大量的 Defi 场景。
笔者实际用下来,感觉Session Key
有点像ChainLink
的Keep
,但是它比Keep
更灵活,且更便宜。
const { LocalAccountSigner } = require("@alchemy/aa-core")const { generatePrivateKey } = require('viem/accounts')const sessionKey = LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey())
以上使用了viem
和@alchemy/aa-core
两个库。
先看代码,我直接在代码里做注解了:
const { SessionKeyProvider, Operation, ParamCondition } = require('@zerodev/sdk')const { getFunctionSelector, pad, zeroAddress } = require('viem')const sessionKeyProvider = await SessionKeyProvider.init({ projectId, // 在 ZeroDev 平台上注册并获得的 ProjectId defaultProvider: ecdsaProvider, // 默认 provider 与 signer 方式,这里使用 ECDSA 作为签名方式 sessionKey, // 上面生成的 Session Key sessionKeyData: { // Session Key 配置参数 validAfter: 0, // 启动任务时间戳 validUntil: 0, // 任务截止时间戳 permissions: [ // 许可条件,是个数组,可以设置多个许可条件 { target: contractAddress, // 交互的合约地址 valueLimit: 0, // 最大发送的 value 值 sig: getFunctionSelector( // 调用合约方法和参数 "mint(address)" ), operation: Operation.Call, // 调用方式,建议使用 Call,最好不要用 DelegateCall rules: [ // 调用参数的条件,也是一个数组,如果有多个参数,可以配置多个参数条件 { condition: ParamCondition.EQUAL, // 条件为“等于”, 其他条件:EQUAL, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, NOT_EQUAL offset: 0, // 因为是第一个参数,偏移量设为 0 param: pad(address, { size: 32 }),// 规定地址的长度为 32 字节 }, ], }, ], paymaster: zeroAddress, // 见下面 }})
关于paymaster
属性:该属性规定了由谁来支付 gas 费。
当字段为空(或zeroAddress
常量时,会话密钥可以在有或没有支付主体的情况下使用(即可以进行任何操作)。注意,这可能是不安全的,因为拥有会话密钥的人可以通过燃气 spam 交易并浪费您所有的以太币,因此只有在某种程度上信任会话密钥的用户时才会这样做。
当字段为address(1)
(或者constants.oneAddress
常量)时,会话密钥必须与支付主体一起使用,但可以是任何支付主体。
当字段为支付主体地址时,则由主账户支付 Gas 费。
类似与标准的ecdsaProvider
一样,见下面代码:
const { hash } = await sessionKeyProvider.sendUserOperation({ target: contractAddress, data: encodeFunctionData({ abi: contractABI, functionName: "mint", args: [address], }),})await sessionKeyProvider.waitForUserOperationTransaction(hash)
Session Key
由于Session Key
是由钱包所有者创建并与持有会话密钥的用户共享的,因此自然会想知道如果所有者和会话密钥用户运行在不同节点的情况下,它们如何通过网络传输。
这里,我们将会话密钥用户称为“代理”,即代表钱包所有者通过会话密钥委托操作的代理。
一般来说,有两种方法可以实现:
所有者创建会话密钥并将其发送给代理。
代理创建一个公私钥对,将公钥发送给所有者以将其“注册”为会话密钥,最后通过私钥使用会话密钥。
第一种方法需要所有者和代理之间较少的通信,但第二种方法更安全,因为会话密钥的私钥部分不离开代理(甚至所有者也无法看到它),因此会话密钥泄漏的可能性较小。
来看一下如何实施这两种方法:
// sessionPrivateKey is the private key of the session keyconst serializedSessionKeyParams = await sessionKeyProvider.serializeSessionKeyParams(sessionPrivateKey)const sessionKeyParams = SessionKeyProvider.deserializeSessionKeyParams(serializedSessionKeyParams)const sessionKeyProvider = await SessionKeyProvider.fromSessionKeyParams({ projectId, sessionKeyParams})
第一步:代理创建公私钥对
const { LocalAccountSigner } = require("@alchemy/aa-core")const sessionPrivateKey = generatePrivateKey()const sessionKey = LocalAccountSigner.privateKeyToAccountSigner(sessionPrivateKey)const sessionPublicKey = await sessionKey.getAddress()
第二步:代理将公钥发给钱包所有人,钱包所有人注册该“公钥”
const { EmptyAccountSigner } = require('@zerodev/sdk')// Create an "empty signer" with the public key aloneconst sessionKey = new EmptyAccountSigner(sessionPublicKey)// create the provider const sessionKeyProvider = await SessionKeyProvider.init({ sessionKey, // the other params, such as the permissions...})const serializedSessionKeyParams = sessionKeyProvider.serializeSessionKeyParams()
第三步:所有人将序列化后的 Session Key 发还给代理,代理解包 Session Key
const sessionKeyParams = { ...SessionKeyProvider.deserializeSessionKeyParams(serializedSessionKey), sessionPrivateKey,}const sessionKeyProvider = await SessionKeyProvider.fromSessionKeyParams({ projectId, sessionKeyParams,})const { hash } = await sessionKeyProvider.sendUserOperation({ // ...use the session key provider as you normally would})
zeroDev 官方推荐使用第二种方法。
最近加密市场进入深熊,很多人都在预测下一轮牛市中的机会赛道。笔者在之前文章中提出:基于Secure Intent-centric Account的Bot
,SocialFi
等大幅度提升用户体验的应用,很可能是非常重要的机会。甚至用这个思路把之前 Web3 的东西重做一遍,都是有价值的。至少最近发生的一切,已经让人感觉到一点东西了。
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。