Skip to content

Session auto-management reuses channel after authorizedSigner changes #471

@deodad

Description

@deodad

Summary

In auto-managed Tempo sessions, mppx reuses an existing channel when the client account's authorizedSigner changes. The next credential is emitted as a voucher for the old channel instead of opening a new channel.

Repro

This only prepares/signs credentials. It does not submit a transaction.

import { Challenge, Credential } from 'mppx'
import { session } from 'mppx/client'
import { createClient, custom } from 'viem'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
import { Account as TempoAccount } from 'viem/tempo'
import { tempoModerato } from 'viem/tempo/chains'

const payer = privateKeyToAccount(generatePrivateKey())
const signerA = TempoAccount.fromSecp256k1(generatePrivateKey(), { access: payer })
const signerB = TempoAccount.fromSecp256k1(generatePrivateKey(), { access: payer })

const payee = privateKeyToAccount(generatePrivateKey()).address
const currency = '0x20c0000000000000000000000000000000000000'
const escrow = '0xe1c4d3dce17bc111181ddf716f75bae49e61a336'

let account = signerA

function getClient() {
  return createClient({
    account,
    chain: tempoModerato,
    transport: custom({
      async request({ method }) {
        if (method === 'eth_chainId') return `0x${tempoModerato.id.toString(16)}`
        if (method === 'eth_fillTransaction')
          return {
            raw: '0x',
            tx: {
              chainId: `0x${tempoModerato.id.toString(16)}`,
              from: payer.address,
              gas: '0x5208',
              input: '0x',
              maxFeePerGas: '0x1',
              maxPriorityFeePerGas: '0x1',
              nonce: '0x0',
              to: escrow,
              type: '0x2',
              value: '0x0',
            },
          }
        throw new Error(`unexpected rpc method: ${method}`)
      },
    }),
  })
}

function challenge() {
  return Challenge.from({
    id: `challenge-${account.accessKeyAddress}`,
    intent: 'session',
    method: 'tempo',
    realm: 'example.test',
    request: {
      amount: '1000000',
      currency,
      methodDetails: { chainId: tempoModerato.id, escrowContract: escrow },
      recipient: payee,
      suggestedDeposit: '10000000',
      unitType: 'token',
    },
  })
}

async function main() {
  const method = session({ getClient, maxDeposit: '10' })

  const first = Credential.deserialize(
    await method.createCredential({ challenge: challenge(), context: {} }),
  )

  account = signerB

  const second = Credential.deserialize(
    await method.createCredential({ challenge: challenge(), context: {} }),
  )

  console.log({
    first: {
      action: first.payload.action,
      authorizedSigner: first.payload.authorizedSigner,
      channelId: first.payload.channelId,
    },
    second: {
      action: second.payload.action,
      authorizedSigner: second.payload.authorizedSigner,
      channelId: second.payload.channelId,
    },
  })
}

main()

Actual

The second credential reuses the first channel and returns a voucher:

{
  first: {
    action: 'open',
    authorizedSigner: '0x...',
    channelId: '0x...'
  },
  second: {
    action: 'voucher',
    authorizedSigner: undefined,
    channelId: '0x...' // same as first
  }
}

Expected

When the account's authorizedSigner changes, auto-managed sessions should not emit a voucher for a channel opened with the previous signer. It should open a distinct channel for the new signer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions