Skip to content

fix(transport): record and drop a peer's user agent under its connection entry - #163

Merged
jacderida merged 4 commits into
WithAutonomi:mainfrom
grumbach:fix/connected-peer-user-agent
Sep 15, 2026
Merged

jacderida merged 4 commits into
WithAutonomi:mainfrom
grumbach:fix/connected-peer-user-agent

Conversation

@grumbach

@grumbach grumbach commented Sep 15, 2026

Copy link
Copy Markdown
Member

Linear issue

Closes V2-1260

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

Moves the user-agent writes inside a lock the transport already takes, and corrects the DHT reconciliation wording that described the old behaviour. No wire, format or routing change.

Compatibility

  • Wire: none.
  • Storage: none.
  • API: none. peer_user_agent keeps its signature; its doc now says a connected peer always has an agent, so None means not connected.

Semver impact

  • breaking
  • feature
  • fix

Test evidence

  • cargo test --lib: 535 passed, 0 failed.
  • cargo clippy --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used (the CI invocation) and cargo fmt --check: clean.
  • The race cannot be forced in a unit test. A concurrent test closing a peer's last channel while registering a second one passed 20,000 rounds on the unfixed code, so it was not added. The fix is structural: registration and both removal paths now write the agent while holding the peer's peer_to_channel entry.
  • Companion: fix(storage): stop counting a peer that disconnects mid-tally as unreported ant-node#228 makes the migration signal skip a peer that disconnected mid-tally. That one is correct without this PR; this one closes the case where a peer stays connected with no agent.
  • Found in review and deliberately not in this PR, since both predate it and need a per-peer lifecycle lock rather than a reordering: a registration can race a channel-loss cleanup and leave a peer mapped to a dead channel with no PeerDisconnected, and PeerDisconnected is sent after the peer's entry is released, so a quick reconnect can emit PeerConnected first. Neither makes a connected peer lose its agent.
  • Related symptom, from the 990-node testnet for ant-node #218: one migration-signal tick in 21,402 counted a peer with no agent as a node that never reported. The mid-tally disconnect behind it is removed by the companion ant-node #228; this PR removes the case where a peer stays connected with no agent.

New dependency

none

ADR

https://github.com/WithAutonomi/ant-node/blob/rc-2026.9.2/docs/adr/ADR-0014-file-based-chunk-store-and-lmdb-retirement.md (the migration signal that reads peer user agents; no saorsa-core ADR covers this bookkeeping)

Mitigation / rollback

Revert the commit. Nothing is persisted, and ant-node's count already tolerates a peer with no agent.

jacderida and others added 3 commits September 8, 2026 22:47
…ion entry

A peer's user agent lived in its own map, written after the peer's entry in
`peer_to_channel` was created and removed after that entry was dropped. Between
the two steps the maps disagreed:

- a registration could land after removal dropped the peer's entry but before it
  dropped the agent, and the removal then deleted the agent the new connection had
  just recorded, leaving a connected peer with no user agent until it next
  disconnected;
- a reader could find a connected peer with no agent recorded yet.

Both writes now happen while holding the peer's `peer_to_channel` entry, on
registration and on both removal paths (`remove_channel_mappings_static` and
`disconnect_peer`), so a connected peer always has its user agent and `None` from
`peer_user_agent` means the peer is not connected. The lock order is unchanged in
kind: the `peer_to_channel` entry is taken first and the agent map inside it, and
nothing takes them the other way round.

ant-node's migration signal counts peers by reading `connected_peers()` and then
each peer's agent; a peer left with no agent is counted as a node that never
reported, which is how a live peer could stay in that count indefinitely.
With the agent recorded and dropped under the peer's connection entry, a peer from the connected_peers snapshot with no agent has disconnected since the snapshot; it is not waiting on its identity announce, and no PeerConnected will follow for it. The doc comment and both log lines said otherwise.

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — reviewed at exact head 13c204cfb9d857d24f6cb6597424a82c59582089.

The peer/user-agent invariant is now established in the right place. On first registration the user-agent is inserted before the peer_to_channel entry becomes visible; on final-channel removal and explicit disconnect it is removed while the same peer_to_channel entry lock is still held. Readers of the connection map therefore cannot observe a connected entry without its agent. I found no reverse peer_user_agentspeer_to_channel acquisition, so the new nested locking does not introduce a deadlock cycle.

The previously disclosed channel_to_peers lifecycle/event-ordering races are pre-existing and are not widened by this change. The DHT reconciliation wording matches the resulting operational semantics.

Verification:

  • cargo test --lib: 535 passed
  • focused authenticated-registration invariant test: passed
  • cargo clippy --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used: passed
  • cargo fmt --check: passed
  • GitHub build/test/lint matrix is green

Non-blocking gap: there is no deterministic connect/disconnect race regression test. The existing registration test covers visibility before event consumers, and the lock/interleaving analysis is sound, so I do not consider the missing stress test a merge blocker.

The failing Security Audit is inherited, not introduced: Cargo.lock is byte-identical to the base and the newly published RUSTSEC-2026-0285 affects that existing rustls version. It should be handled by the release train in a separate dependency bump to rustls >=0.23.45.

@jacderida
jacderida changed the base branch from rc-2026.9.2 to main September 15, 2026 22:43
@jacderida
jacderida merged commit fbac6d0 into WithAutonomi:main Sep 15, 2026
19 of 20 checks passed
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.

3 participants