Mix Transport Implementation Walk Through - Session Lifecycle Events

Why MixTransport publishes its own session events

An established MixTransport session is the anonymous equivalent of an application peer relationship. The session can carry several virtual application streams, and closing one stream does not end that relationship.

A libp2p Switch cannot describe this lifecycle. Switch peer events report authenticated, multiplexed connections between concrete libp2p nodes. In a Mix deployment, those physical connections normally join adjacent relay nodes. A relay connection does not prove that the remote relay has opened an anonymous application session with the local node.

MixTransport therefore publishes session events directly. The implementation does not synthesize libp2p PeerEvent values and does not insert the session pseudonym into Switch.connManager.

Public event API

The public types are defined in libp2p_mix_transport/transport.nim and exported through the package facade:

type
  SessionEventKind* {.pure.} = enum
    Established
    Closed
 
  SessionEvent* = object
    kind*: SessionEventKind
    peerId*: PeerId
    sessionId*: PeerId
    role*: SessionRole
 
  SessionEventHandler* = proc(event: SessionEvent): Future[void] {.
    gcsafe, async: (raises: [CancelledError])
  .}

Established means that the anonymous session handshake succeeded on the endpoint publishing the event. Closed means that MixTransport has removed the session from its registry and completed local session shutdown.

Consumers register and remove handlers through:

proc addSessionEventHandler*(
    self: MixTransport, handler: SessionEventHandler
) {.raises: [].}
 
proc removeSessionEventHandler*(
    self: MixTransport, handler: SessionEventHandler
) {.raises: [].}

Registration ignores a nil handler. The transport stores handlers in an OrderedSet, so registering the same procedure value more than once does not create duplicate callbacks. Registration does not replay events for sessions that were already established; a consumer that needs a complete lifecycle view must register before the first call to connect and before accepting incoming Connect frames.

When an event is published, MixTransport starts every registered handler and waits for all handler futures to finish. A handler can therefore update the consumer’s peer registry before the transport operation that published the event completes. Handlers should perform the required state transition promptly and delegate unrelated long-running work to tasks owned by the consumer.

The peer identity carried by an event

Every event carries both peerId and sessionId because the two values have different meanings on the session initiator.

Publishing endpointevent.peerIdevent.sessionId
InitiatorThe real PeerId of the Mix destination passed to connectThe random session pseudonym generated by the initiator
RecipientThe session pseudonym received in ConnectThe same session pseudonym

The initiator already knows which destination it selected, so higher-level code should continue to identify that peer by the real destination ID. Mix prevents the recipient from learning the initiator’s authenticated libp2p identity, so the recipient identifies the anonymous application peer by the session pseudonym.

event.role makes this distinction explicit without requiring a consumer to compare the two identifiers:

case event.role
of SessionRole.Initiator:
  # event.peerId is the real destination.
  discard
of SessionRole.Recipient:
  # event.peerId is the anonymous session pseudonym.
  discard

The recipient pseudonym is valid only inside the MixTransport application layer. The pseudonym must not be passed to Switch.connect, Switch.dial, or another API that expects an authenticated libp2p peer.

Establishment on the initiator

MixTransport.connect(destination) creates a pending session, sends Connect, and waits for a valid ConnectAck. The raw SURB reply path recovers and decodes the acknowledgement, applies the recipient’s initial SURB-supply snapshot, marks the session established and starts the initiator’s SURB supplier.

After the wait in connect completes, connect publishes Established before returning the session:

if not await session.waitUntilEstablished().withTimeout(self.connectTimeout):
  return err("MixTransport connect timed out")
if session.state != SessionState.Established:
  return err("MixTransport session closed while connecting")
 
keepSession = true
await self.publishSessionEvent(session, SessionEventKind.Established)
ok(session)

The caller therefore receives the successful TransportSession only after all registered establishment handlers have completed.

A later connect(destination) call reuses the established session. The reuse path calls the same idempotent publisher before returning:

self.sessions.getByDestination(destination).withValue(existing):
  if existing.state == SessionState.Established:
    await self.publishSessionEvent(existing, SessionEventKind.Established)
    return ok(existing)

The call is intentional even though the event normally has already been published. The publisher’s session-ID set prevents a second event, while the call also covers an established session whose first connect task had not yet reached event publication.

Establishment on the recipient

The recipient creates a session after validating the incoming Connect frame and its acknowledgement SURBs. The recipient must accept early session traffic as soon as the first ConnectAck copy can reach the initiator, so handleConnect marks the internal session state established before submitting the redundant acknowledgement copies.

The external event has a stricter success condition. handleConnect publishes Established only after sendWithSurbRedundancyBatch reports that at least one ConnectAck copy was submitted successfully:

session.establish()
(await self.sendWithSurbRedundancyBatch(replyBatch, payload)).isOkOr:
  return
keepSession = true
await self.publishSessionEvent(session, SessionEventKind.Established)

If every acknowledgement submission fails, handleConnect removes the provisional session and publishes no lifecycle event. The remote initiator cannot have established that session through a successfully submitted acknowledgement.

Exactly one establishment and one closure

MixTransport.publishedSessionIds records sessions for which Established has been claimed for publication. publishSessionEvent updates this set before invoking handlers:

case kind
of SessionEventKind.Established:
  if session.sessionId in self.publishedSessionIds:
    return
  self.publishedSessionIds.incl(session.sessionId)
of SessionEventKind.Closed:
  if session.sessionId notin self.publishedSessionIds:
    return
  self.publishedSessionIds.excl(session.sessionId)

This state gives the event sequence two useful properties:

  • session reuse and duplicate control packets cannot publish another Established;
  • a pending handshake that fails before publication cannot publish Closed.

Removing the identifier before invoking close handlers also prevents two teardown paths that converge on the same session from publishing duplicate Closed events.

Closure paths

removeAndShutdownSession is the common local transition for an established session:

proc removeAndShutdownSession(
    self: MixTransport, session: TransportSession
) {.async: (raises: [CancelledError]).} =
  discard self.sessions.remove(session.sessionId)
  discard self.replyCredentials.removeSession(session.sessionId)
  await session.shutdown()
  await self.publishSessionEvent(session, SessionEventKind.Closed)

The helper first makes the session unavailable for new frame routing and destination-based reuse. The helper then removes the session’s reply credentials, shuts down the SURB supplier and every session-owned stream, and finally publishes Closed. A close handler therefore observes a peer whose transport resources have already been detached.

The common helper is used by:

  • local disconnect(session) after the Disconnect frame has been submitted;
  • local resetSession(session) after its best-effort ResetSession submission;
  • remote Disconnect after the last stream has closed;
  • remote ResetSession;
  • the initiator’s liveness failure after all configured SURB status probes go unanswered.

MixTransport.stop detaches all sessions before unregistering its Mix handlers. After the best-effort ResetSession submissions, the transport shuts the detached sessions down and publishes one Closed event for every session that previously published Established.

Streams do not produce session events

dial and incoming OpenStream handling create TransportStream objects inside an established session. Opening or closing one of these streams does not change the application peer represented by the session, so stream operations do not publish SessionEvent.

This distinction is important for consumers such as block exchange. Two independent block-exchange streams to the same destination still belong to one transport peer. The consumer removes that peer only when the complete session publishes Closed.