Skip to content

Implement IDONTWANT suppression for partial-message peers (step 7)#468

Draft
lucassaldanha wants to merge 11 commits into
libp2p:developfrom
lucassaldanha:partial-messages-step-7
Draft

Implement IDONTWANT suppression for partial-message peers (step 7)#468
lucassaldanha wants to merge 11 commits into
libp2p:developfrom
lucassaldanha:partial-messages-step-7

Conversation

@lucassaldanha

Copy link
Copy Markdown
Collaborator

Summary

  • Per §5.2 of the partial-messages spec: when we (gossipRouter) have requested partial messages for a topic T and a peer supports sending partial for T, IDONTWANT is not sent to that peer. Sending it would be redundant — the peer is expected to deliver partial RPCs instead of full messages.
  • Extends PubsubProtocol.supportsIDontWant() to include Gossip_V_1_3, since v1.3 is a superset of v1.2 and both IDONTWANT and the Extensions control message must coexist for partial-message sessions.

Changes

  • PubsubProtocol.ktsupportsIDontWant() now returns true for V1.2 and V1.3.
  • GossipRouter.ktiDontWant() gains a .filterNot { shouldSkipIDontWantForPeer(it, msg.topics) } step; new private helper shouldSkipIDontWantForPeer implements the three-level check (extension enabled → peer supports partial at node level → we request partial AND peer supports sending for the topic).
  • PartialMessagesIDontWantSuppressionTest.kt — 4 new unit tests covering: suppression when all conditions are met; IDONTWANT still sent when extension disabled; still sent when we don't request partial; still sent when peer has no topic-level supportsSendingPartial.

Test plan

  • PartialMessagesIDontWantSuppressionTest — all 4 tests pass
  • Full gossip test suite — no regressions

Captures the MVP scope, jvm-libp2p/client responsibility boundary,
client-facing API, routing semantics, per-group lifecycle and DoS
caps, and the implementation plan for the gossipsub partial-messages
extension. Lands ahead of implementation so sub-issues of libp2p#435 can
reference a stable design anchor.
Step 1 of the partial-messages extension: plumb SubOpts.requestsPartial /
SubOpts.supportsSendingPartial through subscribe announcements in both
directions, and track the per-peer-per-topic receive state.

- AbstractRouter parses the flags with the spec-mandated coercion
  (supportsSendingPartial := requestsPartial || supportsSendingPartial)
  and zeroes them on subscribe=false.
- New enqueueSubscribe hook unifies outbound subscribe enqueueing so
  GossipRouter can attach per-topic flags in a single override.
- GossipRouter exposes setTopicPartialFlags(topic, ...) to configure
  flags advertised for a locally-subscribed topic, and stores inbound
  flags in a new PartialSubscriptionState (plain HashMap on the pubsub
  event loop). State is cleaned on peer disconnect, topic unsubscribe,
  and per-peer unsubscribe.
- Outbound unsubscribe MUST NOT carry partial flags; enforced at the
  SubscriptionPart wire-build site.

No routing behaviour changes yet. See docs/partial-messages.md §4.5,
§5, §6.1 for context.
…t addSubscription overload

- `PartialSubscriptionWireTest`: route reads of `partialSubscriptionState`
  through `submitOnEventThread { ... }.join()`. The state container is
  not thread-safe; direct access from the JUnit thread races the event
  loop and can surface as `ConcurrentModificationException` or stale
  reads. Two helpers (`peerFlagsOnEventLoop`, `snapshotPartialStateOnEventLoop`)
  establish the happens-before barrier.

- `RpcPartsQueue`: remove the 2-arg `addSubscription(topic, status)`
  default overload. The remaining 4-arg abstract method is the single
  source of truth; `addSubscribe` / `addUnsubscribe` remain the
  convenience entry points.
- `PartialSubFlags.coerce(requestsPartial, supportsSendingPartial)`:
  single source of truth for the spec coercion rule
  `supportsSendingPartial := requestsPartial || supportsSendingPartial`.
  Used from `GossipRouter.setTopicPartialFlags` for the outbound side.
  AbstractRouter keeps the inline expression for the receive side to
  avoid a reverse layering dependency (pubsub -> gossip); a comment
  notes the rule is applied on both sides.

- `PartialSubscriptionState.setPeerFlags`: document that passing
  `PartialSubFlags.NONE` (or any equivalent all-false flags) is
  treated as a removal. Makes the set-sometimes-deletes invariant
  explicit for readers.

- `AbstractRouter.handleMessageSubscriptions`: add Kdoc now that the
  method is `protected open`. Documents the "call super" contract
  for overrides (GossipRouter relies on this to keep peersTopics and
  partialSubscriptionState in sync) and the flag-normalisation
  precondition.
Introduces the public partial-messages API surface and the internal
state management layer required before any routing logic lands:

Public API (io.libp2p.pubsub.gossip.partialmessages):
- PartialMessagesHandler<PeerState> — onIncomingRpc + onEmitGossip;
  PartialMessagesPeerFeedback passed per-call (resolves open question
  from design doc §9)
- PublishAction<PeerState> / PublishActionsFn<PeerState>
- PartialMessagesPeerFeedback interface + FeedbackKind enum

Internal state management:
- GroupId — content-equality ByteArray wrapper for use as map key
- GroupState<PeerState> — per-(topic,groupId) container with mutable
  TTL and app-opaque peerStates
- PartialGroupStateStore<PeerState> — TTL countdown, GC on ttl≤0 or
  empty peerStates, DoS caps (255/topic, 8/topic/peer, matching
  go-libp2p defaults), onPeerDisconnected, onTopicUnsubscribed
- PartialMessagesAdapter (internal interface) /
  PartialMessagesAdapterImpl<PeerState> — erases PeerState at the
  GossipRouter boundary via a single @Suppress("UNCHECKED_CAST")
  in the builder

Wiring:
- GossipRouterBuilder: partialMessagesHandler field; build-time error
  if PARTIAL_MESSAGES extension enabled without a handler
- GossipRouter: internal var partialMessages: PartialMessagesAdapter?

No routing changes in this step.
Replaces the stub in GossipRouter.processPartialMessageExtension with
the full flow: drop RPCs missing topicID or groupID, then delegate to
PartialMessagesAdapterImpl which gets-or-creates the GroupState (with
DoS cap enforcement) and calls handler.onIncomingRpc with the live
peerStates map.
Adds the outbound path for the partial-messages extension:

- GossipRpcPartsQueue: addPartialMessage queues a PartialMessagePart;
  takeMerged caps at 1 per RPC (proto field is optional, not repeated).
- PartialMessagesAdapter: publishPartial invokes the client's
  PublishActionsFn, enforces the spec MUST (omit partialMessage when peer
  supports but did not request), updates nextPeerState atomically, and
  calls back via enqueueFn.
- GossipRouter: publishPartial looks up PeerHandler by PeerId, routes
  through GossipRpcPartsQueue (not a direct send), and flushes pending.
- Gossip facade: publishPartial submits to the event thread and returns
  CompletableFuture<Unit>.

Tests: PartialMessagesOutboundRpcTest (5 wire-level) and 6 new unit
tests in PartialMessagesAdapterImplTest.
Exercises the full stack built in steps 1-4 — SubOpts flag plumbing,
ControlExtensions handshake, inbound handler dispatch, group-state
tracking, and outbound publishPartial — over real TCP/Noise/Mplex
using a trivial ByteArray bitmap as PeerState.

Four tests:
- Unidirectional partial RPC (payload + metadata delivered to handler)
- Bidirectional round-trip between two hosts
- nextPeerState persisted and visible to subsequent decide() calls
- Spec MUST: partialMessage omitted when peer supports but did not request

Both hosts bind to port 0 to avoid conflicts with other tests.
Per §5.2 of the partial-messages spec: when we have requested partial
messages for a topic T and a peer supports sending partial for T, skip
IDONTWANT to that peer. Sending IDONTWANT is redundant — the peer is
expected to deliver partial RPCs instead of full messages.

Also extends PubsubProtocol.supportsIDontWant() to include Gossip_V_1_3,
since v1.3 is a superset of v1.2 and both IDONTWANT and the Extensions
control message must coexist for partial-message sessions.
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.

1 participant