Skip to content

feat: run identify on every new connection and populate the peer store - #273

Merged
adust09 merged 1 commit into
mainfrom
feat/267-identify-on-connect
Aug 23, 2026
Merged

feat: run identify on every new connection and populate the peer store#273
adust09 merged 1 commit into
mainfrom
feat/267-identify-on-connect

Conversation

@adust09

@adust09 adust09 commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Closes #267. Unblocks #258.

Problem

swPeerStore was written only by handleIdentifyPush, i.e. only when a remote pushed to us. requestIdentify returned IdentifyInfo without storing it, and nothing invoked it when a connection came up — Dial.hs and Listen.hs contain no reference to it. So nothing was known about a peer we dialled or accepted.

Spec

Identify is how a peer learns the remote's public key, listen addresses, protocols and the address it observes for us. specs/relay/DCUtR depends on it having already run:

"Upon observing the new connection, the inbound peer (here B) checks the addresses advertised by A via identify."

go-libp2p drives its IDService from the swarm's Connected notification for the same reason.

Change

  • registerIdentifyHandlers additionally subscribes an on-connect notifier on swNotifiers. Any Switch that registers identify gets this with no further wiring, and the Switch core keeps no dependency on the protocol — the same shape GossipSub already uses.
  • The notifier is a thin wrapper over identifyPeer :: Switch -> Connection -> IO (Either String ()), which is exported. Callers that need to observe the outcome — tests, and the DCUtR integration in dcutr: production integration reuses the relay connection instead of dialing direct #258 — call it directly and read the result. The notifier itself discards it.
  • Storing goes through a shared storeIdentify, used by both the push responder and identifyPeer, so both merge into what is already known via mergeIdentify rather than replacing it. A push is a partial update and must not erase fields it omits.
  • Runs on inbound and outbound connections alike, and over relayed connections — which is what dcutr: production integration reuses the relay connection instead of dialing direct #258 needs. go-libp2p likewise allows identify over limited connections (network.WithAllowLimitedConn(ctx, "identify")).
  • No retry. A peer that does not answer now is picked up by a later push, if it sends one.

Stream lifetime

requestIdentify did not close its stream. That was harmless while it had no callers; running it per connection would leak a half-open stream and a thread for every connection, since a peer may negotiate and then never answer.

It now closes on every exit path — success, negotiation refusal, decode failure, timeout, exception — and bounds the exchange with a 5s timeout, matching go-libp2p's identify.DefaultTimeout (which it applies to all id interactions in both directions). Its signature is unchanged; a timeout surfaces as Left.

The rest of the stream-ownership work stays with #257: AutoNAT on both sides, and dispatchStream's ownership model, which affects every protocol and is not something this PR should decide.

Deliberately not included

A wait-for-identify primitive. go-libp2p has IdentifyWait, and its hole punch service takes the identify service as a required dependency to wait on it. Here the identify notifier and #258's DCUtR notifier will both fire on the same connection with no ordering guarantee — but #258 can resolve that deterministically by calling identifyPeer itself and reading the result, without new Switch state to keep and clean up. If #258 turns out to need a shared completion signal, it can be added then against concrete requirements rather than guessed at now.

Tests

New LibP2P.Protocol.Identify.IdentifyOnConnectSpec, two real switches over loopback TCP:

  • the dialer's peer store holds the remote's listen addresses and protocol version after a dial
  • the listener's peer store is likewise populated for an accepted connection
  • a remote with no identify handler yields a failure, records nothing, and leaves the connection usable for a further exchange
  • a live exchange merges into a pre-seeded entry instead of replacing it
  • the entry is keyed by the peer id the connection was authenticated as

Full suite: 1159 examples, 0 failures.

Note on verification

The Haskell toolchain is unavailable on this host, so the build and test runs were done in the repo's own haskell:9.10-slim-bookworm image.

swPeerStore was only ever written by handleIdentifyPush, so it filled
only when a remote pushed to us. requestIdentify returned IdentifyInfo
without storing it, and nothing called it when a connection came up, so
nothing was known about a peer we dialled or accepted.

specs/relay/DCUtR depends on this being populated: "Upon observing the
new connection, the inbound peer (here B) checks the addresses advertised
by A via identify." go-libp2p drives its IDService from the swarm's
Connected notification for the same reason.

registerIdentifyHandlers now also subscribes an on-connect notifier, so
any Switch that registers identify gets this without further wiring and
the Switch core keeps no dependency on the protocol. The notifier is a
thin wrapper over identifyPeer, which is exported so callers that need to
observe the outcome -- tests, and the DCUtR integration in #258 -- can
run it directly and read the result.

requestIdentify now closes its stream on every exit path and bounds the
exchange with a 5s timeout, matching go-libp2p's identify.DefaultTimeout.
Running it per connection would otherwise leak a half-open stream and a
thread for every connection, since a peer can negotiate and then never
answer. The remaining stream-ownership work -- AutoNAT both sides, and
dispatchStream's ownership model -- stays with #257.

Storing goes through a shared storeIdentify used by both the push
responder and identifyPeer, so both merge into what is already known via
mergeIdentify instead of replacing it.

Unblocks #258.
Closes #267.
@adust09
adust09 merged commit 4a37670 into main Aug 23, 2026
3 checks passed
@adust09
adust09 deleted the feat/267-identify-on-connect branch August 23, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

identify: run identify on every new connection and populate the peer store

1 participant