jackygu's blog

发布于 2023-10-16到 Mirror 阅读

使用Session Key委托服务器安全的操作抽象账户

最近电报自动交易机器人和各种SocialFi很火,这些产品给用户带来了类似Web2的良好用户体验。但火爆的背后,也发生了多起安全事件。为此,很多新上线的平台开始使用更先进的账户安全技术来保护用户资产,比如@tomo_social使用了ERC-4337账户抽象技术,有些电报机器人采用了MPC钱包技术。

尽管账户抽象钱包(AA钱包)已经具备了零gas费(服务商代付gas费),多签,社交登录等强大功能,并大幅度提升用户体验,但是因为ERC-4337属于在现有以太坊共识基础上的补丁方案,与链交互签名时仍旧需要私钥,各种方案只是在私钥保存和签名环节采取各种安全措施。

所以,虽然很多代用户签名交互的电报机器人,SocialFi平台通过MPC钱包AA钱包来保障客户的私钥安全,但实际上,因为最终还是要通过钱包主私钥来进行签名,本质上还是私钥的验证模式,所以仍旧有私钥泄露的风险。

今天看到AA钱包创新项目ZeroDevSession Key(对话密钥)解决方案,可以让AA钱包授权生成一个或若干个Session Key(也是一种私钥),来受控的执行经授权的操作。这种授权模式有别于ERC-20ERC-721的合约资产的使用额度授权,更有别于私钥的验证模式,一旦授权Session Key后,可以通过观察者(如服务器)自动执行授权范围内的合约或链上交互操作,从而可以带来更安全更便捷的用户体验。

下面内容参考:https://docs.zerodev.app/use-wallets/use-session-keys

1- 什么是Session Key

会话密钥(Session keys)是ZeroDevAA钱包中最强大的功能之一,它具有许多很多实际的应用场景,能解决很多痛点。

传统的EOA钱包只有一个用来为钱包签署交易的密钥(私钥)。如果拥有该密钥,就拥有了该钱包。这就是为什么绝对不能丢失或泄露助记词(seed phrase)或私钥的原因。

然而,使用AA钱包时,钱包与密钥是分离的,即:钱包所有者可以为任意密钥指定发起交易的权限,并随时撤销这些密钥的访问权限,甚至还可以限定密钥的范围,使其只能在特定条件和特定时间窗口内发送有限的交易。

我们称这些受到主密钥控制的密钥为会话密钥

有一篇ZeroDev创始人写的文章可以参考:《会话密钥是 Web3 的 JWT》(Session Keys are the JWTs of Web3)

2- Session Key的用途

Session Key有几个核心用途:

2.1- 交易无需确认

如果您正在构建一个高频交互的Dapp,可能希望用户不必频繁的手动确认每个交易。这时,可以为用户的当前“会话”创建一个Session Key,并限定该Key的使用时间和范围,使其只能发送您所允许的交易,并且该Key在当前会话结束后失效。设定后,可以使用该Session Key与Dapp进行交互,而无需使用他们的主密钥对每一个交易都进行手动确认。

2.2- 委托交易

通常情况下,交易需要由钱包所有者主动发起。然而,有时候“自动化”交易能实现更佳的用户体验(如电报机器人等)。例如,如果您正在构建一个电报交易bot,您可能希望为用户提供一个功能,即当价格接近设定的目标价格时,自动执行买入或卖出的交易。在这种情况下,可以创建一个Session Key,仅当价格确实接近目标价时才允许执行。把这个bot部署到一个“观察者”(比如服务器)共享会话密钥,当条件发生时,观察者(服务器)会为用户自动执行交易。

这类应用场景非常大,不仅仅包括自动交易机器人实现的挂单交易,跟随交易,自动抢单,还包括大量的Defi场景。

笔者实际用下来,感觉Session Key有点像ChainLinkKeep,但是它比Keep更灵活,且更便宜。

3- 代码实践

3.1- 生成Session Key

const { LocalAccountSigner } = require("@alchemy/aa-core")
const { generatePrivateKey } = require('viem/accounts')

const sessionKey = LocalAccountSigner.privateKeyToAccountSigner(generatePrivateKey())

以上使用了viem@alchemy/aa-core两个库。

3.2- 配置Session Key

先看代码,我直接在代码里做注解了:

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费。

3.3- 使用Session Key

类似与标准的ecdsaProvider一样,见下面代码:

const { hash } = await sessionKeyProvider.sendUserOperation({
  target: contractAddress,
  data: encodeFunctionData({
    abi: contractABI,
    functionName: "mint",
    args: [address],
  }),
})

await sessionKeyProvider.waitForUserOperationTransaction(hash)

4- 如何在网上安全的分享Session Key

由于Session Key是由钱包所有者创建并与持有会话密钥的用户共享的,因此自然会想知道如果所有者和会话密钥用户运行在不同节点的情况下,它们如何通过网络传输。

这里,我们将会话密钥用户称为“代理”,即代表钱包所有者通过会话密钥委托操作的代理。

一般来说,有两种方法可以实现:

  • 所有者创建会话密钥并将其发送给代理。

  • 代理创建一个公私钥对,将公钥发送给所有者以将其“注册”为会话密钥,最后通过私钥使用会话密钥。

第一种方法需要所有者和代理之间较少的通信,但第二种方法更安全,因为会话密钥的私钥部分不离开代理(甚至所有者也无法看到它),因此会话密钥泄漏的可能性较小。

来看一下如何实施这两种方法:

4.1- 所有者创建方式

// sessionPrivateKey is the private key of the session key
const serializedSessionKeyParams = await sessionKeyProvider.serializeSessionKeyParams(sessionPrivateKey)

const sessionKeyParams = SessionKeyProvider.deserializeSessionKeyParams(serializedSessionKeyParams)

const sessionKeyProvider = await SessionKeyProvider.fromSessionKeyParams({
  projectId,
  sessionKeyParams
})

4.2- 代理创建并由钱包所有者注册方式

第一步:代理创建公私钥对

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 alone
const 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 AccountBotSocialFi等大幅度提升用户体验的应用,很可能是非常重要的机会。甚至用这个思路把之前Web3的东西重做一遍,都是有价值的。至少最近发生的一切,已经让人感觉到一点东西了。