identifyWait method

  1. @override
Future<void> identifyWait(
  1. Conn conn
)
override

IdentifyWait triggers an identify (if the connection has not already been identified) and returns a future that completes when the identify protocol completes.

Implementation

@override
Future<void> identifyWait(Conn conn) async {
  final peerId = conn.remotePeer;
  final identifyWaitStart = DateTime.now();

  Completer<void>? completerToAwait;

  await _connsMutex.synchronized(() async {
    final mutexAcquiredTime = DateTime.now().difference(identifyWaitStart);

    var entry = _conns[conn];

    if (entry != null) {
      // OPTIMIZATION: If identify already succeeded for this connection, return immediately.
      // This prevents re-running identify on every newStream call, which would cause
      // 30-second timeouts when the connection is stale.
      if (entry.identifySucceeded) {
        _log.fine(
            ' [IDENTIFY-WAIT-ALREADY-SUCCEEDED] Peer $peerId already identified on this connection, skipping');
        // completerToAwait remains null, so function will return immediately
        return;
      }

      if (entry.identifyWaitCompleter != null &&
          !entry.identifyWaitCompleter!.isCompleted) {
        completerToAwait = entry.identifyWaitCompleter;
        // No need to spawn _identifyConn again if one is already running for this entry.
      } else {
        entry.identifyWaitCompleter = Completer<void>();
        completerToAwait = entry.identifyWaitCompleter;
        // Spawn _identifyConn as this is a new request for this entry or previous one completed/failed.
        _spawnIdentifyConn(conn, entry);
      }
    } else {
      if (conn.isClosed) {
        _log.warning(
            ' [IDENTIFY-WAIT-PHASE-1-CONN-CLOSED] Connection to peer=$peerId is already closed. Not creating entry or starting identify.');
        // Completer to await will remain null, function will return.
        return;
      }

      entry = _addConnWithLock(
          conn); // _addConnWithLock returns the new/existing entry
      entry.identifyWaitCompleter = Completer<void>();
      completerToAwait = entry.identifyWaitCompleter;
      _spawnIdentifyConn(conn, entry);
    }
  });

  final mutexReleasedTime = DateTime.now().difference(identifyWaitStart);

  if (completerToAwait == null) {
    _log.warning(
        ' [IDENTIFY-WAIT-NO-COMPLETER] No completer to await for peer=$peerId (e.g., connection was closed). Identify will not complete.');
    return; // Or throw, depending on desired behavior for closed conns.
  }

  try {
    await completerToAwait!.future;
    final totalDuration = DateTime.now().difference(identifyWaitStart);
  } catch (e, st) {
    final totalDuration = DateTime.now().difference(identifyWaitStart);
    _log.warning(
        ' [IDENTIFY-WAIT-ERROR] Identify completer for peer=$peerId completed with error, total_duration=${totalDuration.inMilliseconds}ms, error=$e\n$st');

    // Handle timeout exceptions gracefully.
    // A timeout indicates the peer is unreachable, not a security concern.
    // The failure event has already been emitted by _spawnIdentifyConn's catchError,
    // so applications can listen for EvtPeerIdentificationFailed if needed.
    // Rethrowing typed IdentifyTimeoutException allows callers to specifically handle timeouts.
    if (e is IdentifyTimeoutException) {
      _log.warning(
          ' [IDENTIFY-WAIT-TIMEOUT] Identify timeout for peer=$peerId handled gracefully. Rethrowing typed exception for caller to handle.');
      rethrow;
    }

    // Also handle timeout detection from generic exceptions
    if (isTimeoutException(e)) {
      _log.warning(
          ' [IDENTIFY-WAIT-TIMEOUT-DETECTED] Identify timeout detected for peer=$peerId. Converting to typed exception.');
      throw IdentifyTimeoutException(
        peerId: peerId,
        message: 'Identify timed out for peer $peerId',
        cause: e,
      );
    }

    // CRITICAL SECURITY FIX: For non-timeout errors, we MUST rethrow.
    // If identify fails (other than timeout), the connection is unverified
    // and potentially insecure (missing keys/metadata).
    // Callers (like BasicHost.connect or newStream) expect identifyWait to complete successfully
    // before proceeding. If we swallow the error, they proceed with a degraded connection.
    // By rethrowing, we ensure BasicHost aborts the connection.
    rethrow;
  }
}