Skip to content

enh/1231-add-gossipsub-1.3-support#1237

Merged
seetadev merged 37 commits intolibp2p:mainfrom
Winter-Soren:enh/1231-add-gossipsub-1.3-support
Mar 31, 2026
Merged

enh/1231-add-gossipsub-1.3-support#1237
seetadev merged 37 commits intolibp2p:mainfrom
Winter-Soren:enh/1231-add-gossipsub-1.3-support

Conversation

@Winter-Soren
Copy link
Copy Markdown
Contributor

@Winter-Soren Winter-Soren commented Feb 21, 2026

py-libp2p currently lacks full implementation of the GossipSub v1.3 specification (Extensions Control Message). The protocol ID /meshsub/1.3.0 is present and some extension plumbing exists, but the wire format, first-message semantics, and “at most once” rules from the spec are not satisfied. This work implements v1.3, and the Topic Observation extension from the ethresear.ch proposal, so py-libp2p nodes can use the spec-shaped control plane and support observers that receive IHAVE-style notifications without full message payloads when both peers use this implementation’s Topic Observation wire layout (see Interop expectations below).

Interop expectations

GossipSub v1.3 extensions (e.g. ControlMessage.extensions field 6, first-message / at-most-once behaviour, test extension where field numbers match) are intended to align with the spec and can plausibly interoperate with other stacks that implement the same v1.3 pieces. Topic Observation (including ControlExtensions.topicObservation, OBSERVE/UNOBSERVE control fields, and immediate IHAVE to observers) follows the ethresearch proposal and is documented in-tree in libp2p/pubsub/pb/rpc.proto; end-to-end Topic Observation with arbitrary go-libp2p-pubsub / nim-libp2p / js-libp2p peers is not guaranteed until those implementations share the same experimental wire definitions or we add explicit interop tests. Unknown protobuf fields should still be ignored by typical decoders, but feature-level interop requires matching schemas.

What was wrong?

Issue #1231 — py-libp2p did not fully implement the GossipSub v1.3 specification (Extensions Control Message) or the Topic Observation extension.

Gaps included:

  • Protobuf: rpc.proto did not match the spec: no ControlExtensions (single message with optional fields per extension), and no Topic Observation / TestExtension messages.
  • First message: The first message on the stream (hello packet) was built without giving the router a chance to add an Extensions control message, so extensions were never sent in the first message.
  • At most once: There was no tracking of “already sent Extensions” per peer, so the “MUST NOT be sent more than once” rule was not enforced.
  • Duplicate Extensions: A peer sending Extensions more than once was not treated as misbehaviour (no downscore).
  • Topic Observation: OBSERVE/UNOBSERVE and immediate IHAVE notifications to observers were not implemented, so observing nodes could not receive notifications without full message payloads.

How was it fixed?

  • Spec-aligned protobuf: Updated libp2p/pubsub/pb/rpc.proto to add ControlExtensions (field 6 on ControlMessage) with topicObservation and testExtension plus ControlObserve, ControlUnobserve, and TestExtension on RPC. Regenerated rpc_pb2.py and rpc_pb2.pyi.

  • Extensions state machine: Added libp2p/pubsub/extensions.py with PeerExtensions, ExtensionsState, and TopicObservationState. ExtensionsState handles:

    • Building our extensions into the first hello via build_hello_extensions(peer_id, hello).
    • Per-peer “sent extensions” tracking (at most once).
    • Parsing peer extensions from the first RPC and treating duplicate Extensions as misbehaviour (callback to scorer).
  • First message injection: In pubsub.py’s _handle_new_peer(), after building the hello packet, we call the router’s extensions_state.build_hello_extensions(peer_id, hello) when the router has extensions_state and supports_v13_features (duck-typing + cast for type-checking). Only v1.3-capable routers add Extensions to the first message.

  • GossipSub v1.3 wiring in gossipsub.py: Introduced PROTOCOL_ID_V13 (/meshsub/1.3.0), included it in add_peer and in protocol checks (e.g. _get_in_topic_gossipsub_peers_from_minus). Router now has extensions_state and topic_observation (TopicObservationState), initialised with optional my_extensions: PeerExtensions. In handle_rpc, we call extensions_state.handle_rpc(rpc, sender_peer_id) first for v1.3 peers, then dispatch handle_observe / handle_unobserve for Topic Observation. On remove_peer, we call extensions_state.remove_peer() and topic_observation.remove_peer().

  • Topic Observation behaviour: Subscribers record observers via topic_observation.add_observer(topic, peer) on OBSERVE and remove them on UNOBSERVE. In publish(), after validating and caching a message, we call _notify_observers(topic_ids, msg_id) to send IHAVE to all observers of those topics immediately (not only at heartbeat). Added emit_observe / emit_unobserve and the corresponding handlers; observers are only accepted when the peer advertised the Topic Observation extension.

  • Misbehaviour on duplicate Extensions: When a peer sends Extensions and we already have their extensions, we call _report_extensions_misbehaviour(peer_id), which uses scorer.penalize_behavior(peer_id, 1.0).

Result: v1.3 protocol ID is supported, Extensions are sent only in the first message and at most once per peer, duplicate Extensions are penalised, and Topic Observation (OBSERVE/UNOBSERVE + immediate IHAVE to observers) is implemented, with a small set of well-scoped stub methods left for new contributors.

To-Do

  • Documentation: docs/gossipsub-1.3.rst
  • Release notes: newsfragments/1231.feature.rst (Towncrier / #1231)
  • Commit history: optional squash/rebase per maintainer preference

Cute Animal Picture

image

@IronJam11 IronJam11 force-pushed the enh/1231-add-gossipsub-1.3-support branch from 149337d to 8af34e4 Compare February 22, 2026 11:47
@seetadev
Copy link
Copy Markdown
Contributor

seetadev commented Mar 2, 2026

@Winter-Soren , @IronJam11 : Neat progress, friends. Lets arrive at a good conclusion on this PR this week.

Please resolve CI/CD issues too.

This is a substantial and very thoughtful upgrade.

Bringing full GossipSub v1.3 support (including proper Extensions handling, “at most once” semantics, misbehaviour scoring, and Topic Observation with immediate IHAVE notifications) is a big step forward for py-libp2p interoperability. The separation via extensions.py and the clean state-machine approach make the implementation feel structured and spec-aligned rather than bolted on.

I especially appreciate:

  • Spec-accurate protobuf updates and regeneration
  • Proper first-message injection for extensions
  • Enforcement + penalization of duplicate Extensions
  • Well-scoped Topic Observation wiring
  • Dedicated test suite for v1.3 extended behavior

Once CI issues and merge conflicts are resolved, this will be a major milestone for pubsub maturity in py-libp2p. Strong progress 🚀

@Winter-Soren Winter-Soren marked this pull request as ready for review March 11, 2026 11:16
@acul71
Copy link
Copy Markdown
Contributor

acul71 commented Mar 14, 2026

@Winter-Soren

Thanks for putting this together. Implementing v1.3 extensions and Topic Observation is a major step forward for py-libp2p.

The overall structure and test additions are solid, but there are a few blockers to address before merge.
I'm not confident about the Protobuf Schema Issue, but for sure you have to add a newsfragment doc file and take a look at other issues. I had the idea that protobuf fields numbering is for field order, but maybe there's more to it.
In Protobuf, the field number is part of the wire format, so changing it would effectively make it a different field and could break backward compatibility with existing messages or implementations. That’s why field numbers are typically treated as stable once assigned. They have to be like in specs

🔴 Critical Blockers

1) Protobuf Schema / Interop Target Needs Clarification

In libp2p/pubsub/pb/rpc.proto, the Topic Observation fields currently use low-number tags and are embedded in ControlMessage/ControlExtensions (topicObservation = 1, observe = 7, unobserve = 8).

  • Issue: This appears to diverge from the current upstream specs v1.3 extension registry shape and current public upstream schemas. Current upstream schemas do not define observe/unobserve there at all.
  • Request: If this PR intentionally targets a newer unpublished extension draft or specific cross-client agreement, please link the authoritative reference in the PR description.
    Otherwise, please align with one explicit wire-format strategy and document interop expectations clearly.

2) Missing Newsfragment

  • Issue: PR references #1231, but no newsfragments/1231.<type>.rst file is present.
  • Fix: Add newsfragments/1231.feature.rst (or .bugfix.rst), user-facing text, newline-terminated.

🟡 Major Issues

3) Message ID Stringification Bug in Observer Path

In libp2p/pubsub/gossipsub.py, _notify_observers() currently does:

msg_id_str = str(msg_id)
await self.emit_ihave(topic, [msg_id_str], observer_peer)
  • Issue: str(bytes) produces "b'...'", not hex text. Downstream handle_ihave() parses with safe_bytes_from_hex, so this can fail and silently drop observer notifications.
  • Fix: Use msg_id.hex(), matching the existing heartbeat path (msg_id_strs = [msg_id.hex() for msg_id in msg_ids]).

4) Protocol-Gating for Hello Extensions

In libp2p/pubsub/pubsub.py, build_hello_extensions() is called based on router attributes, but not explicitly gated by the negotiated protocol version.

  • Issue: v1.3 extension fields can be attached to peers that did not negotiate /meshsub/1.3.0.
  • Fix: Gate extension injection by negotiated protocol capability (v1.3+ only).

📝 Minor / Follow-up

  • Add a short docs example for Topic Observation lifecycle (start_observing_topic, stop_observing_topic) and expected IHAVE-only observer behavior.

Validation Snapshot

  • make lint: pass
  • make typecheck: pass
  • make test: pass (2464 passed, 16 skipped)
  • make linux-docs: pass

Overall Assessment

  • Quality Rating: Needs fixes
  • Security Impact: Low to Medium
  • Merge Readiness: Needs fixes
  • Confidence: High

@acul71
Copy link
Copy Markdown
Contributor

acul71 commented Mar 21, 2026

Hey @Winter-Soren do you need help here ?

@seetadev
Copy link
Copy Markdown
Contributor

@Winter-Soren : Hi Soham, this is a substantial and very thoughtful piece of work 👏 Bringing full GossipSub v1.3 support (including Extensions, “at most once” semantics, misbehaviour handling, and Topic Observation) is a big step forward for interoperability and overall pubsub maturity in py-libp2p.

The structure with extensions.py and the state-machine approach feels clean and well-aligned with the spec, and it’s great to see the earlier feedback incorporated (protobuf alignment, hex fix, protocol gating, and newsfragment). At this point, the main blocker is getting CI back to green across environments, once that’s resolved, I think we’re in a strong position to land this.

Let’s aim to close this out this week, happy to help unblock on CI or final polish if needed.

CCing @acul71 for his review and feedback. We should land it in the coming release.

@acul71
Copy link
Copy Markdown
Contributor

acul71 commented Mar 25, 2026

CCing @acul71 for his review and feedback. We should land it in the coming release.

Hi @seetadev, @Winter-Soren Is not responding, should I take over ?

@Winter-Soren
Copy link
Copy Markdown
Contributor Author

@Winter-Soren

Thanks for putting this together. Implementing v1.3 extensions and Topic Observation is a major step forward for py-libp2p.

The overall structure and test additions are solid, but there are a few blockers to address before merge. I'm not confident about the Protobuf Schema Issue, but for sure you have to add a newsfragment doc file and take a look at other issues. I had the idea that protobuf fields numbering is for field order, but maybe there's more to it. In Protobuf, the field number is part of the wire format, so changing it would effectively make it a different field and could break backward compatibility with existing messages or implementations. That’s why field numbers are typically treated as stable once assigned. They have to be like in specs

🔴 Critical Blockers

1) Protobuf Schema / Interop Target Needs Clarification

In libp2p/pubsub/pb/rpc.proto, the Topic Observation fields currently use low-number tags and are embedded in ControlMessage/ControlExtensions (topicObservation = 1, observe = 7, unobserve = 8).

  • Issue: This appears to diverge from the current upstream specs v1.3 extension registry shape and current public upstream schemas. Current upstream schemas do not define observe/unobserve there at all.
  • Request: If this PR intentionally targets a newer unpublished extension draft or specific cross-client agreement, please link the authoritative reference in the PR description.
    Otherwise, please align with one explicit wire-format strategy and document interop expectations clearly.

2) Missing Newsfragment

  • Issue: PR references #1231, but no newsfragments/1231.<type>.rst file is present.
  • Fix: Add newsfragments/1231.feature.rst (or .bugfix.rst), user-facing text, newline-terminated.

🟡 Major Issues

3) Message ID Stringification Bug in Observer Path

In libp2p/pubsub/gossipsub.py, _notify_observers() currently does:

msg_id_str = str(msg_id)
await self.emit_ihave(topic, [msg_id_str], observer_peer)
  • Issue: str(bytes) produces "b'...'", not hex text. Downstream handle_ihave() parses with safe_bytes_from_hex, so this can fail and silently drop observer notifications.
  • Fix: Use msg_id.hex(), matching the existing heartbeat path (msg_id_strs = [msg_id.hex() for msg_id in msg_ids]).

4) Protocol-Gating for Hello Extensions

In libp2p/pubsub/pubsub.py, build_hello_extensions() is called based on router attributes, but not explicitly gated by the negotiated protocol version.

  • Issue: v1.3 extension fields can be attached to peers that did not negotiate /meshsub/1.3.0.
  • Fix: Gate extension injection by negotiated protocol capability (v1.3+ only).

📝 Minor / Follow-up

  • Add a short docs example for Topic Observation lifecycle (start_observing_topic, stop_observing_topic) and expected IHAVE-only observer behavior.

Validation Snapshot

  • make lint: pass
  • make typecheck: pass
  • make test: pass (2464 passed, 16 skipped)
  • make linux-docs: pass

Overall Assessment

  • Quality Rating: Needs fixes
  • Security Impact: Low to Medium
  • Merge Readiness: Needs fixes
  • Confidence: High

Thank you so much @acul71 for your comprehensive feedback,
I have addressed them in the commit d4cc6e7

@Winter-Soren
Copy link
Copy Markdown
Contributor Author

CCing @acul71 for his review and feedback. We should land it in the coming release.

Hi @seetadev, @Winter-Soren Is not responding, should I take over ?

Hey @acul71
Pardon, I am working on resolving CI/CD issues!
Some test cases keep failing locally, so taking slightly more time than usual,
Hoping to have this PR ready in a day!

@Winter-Soren
Copy link
Copy Markdown
Contributor Author

@Winter-Soren : Hi Soham, this is a substantial and very thoughtful piece of work 👏 Bringing full GossipSub v1.3 support (including Extensions, “at most once” semantics, misbehaviour handling, and Topic Observation) is a big step forward for interoperability and overall pubsub maturity in py-libp2p.

The structure with extensions.py and the state-machine approach feels clean and well-aligned with the spec, and it’s great to see the earlier feedback incorporated (protobuf alignment, hex fix, protocol gating, and newsfragment). At this point, the main blocker is getting CI back to green across environments, once that’s resolved, I think we’re in a strong position to land this.

Let’s aim to close this out this week, happy to help unblock on CI or final polish if needed.

CCing @acul71 for his review and feedback. We should land it in the coming release.

Thank you so much @seetadev

… align pubsub IHAVE tests with hex message IDs
@Winter-Soren
Copy link
Copy Markdown
Contributor Author

The Read the Docs job is failing before Sphinx/docx even starts: RTD is trying to check out commit ba224a8..., but that SHA is no longer available in the fetched PR history (fatal: reference is not a tree). This usually happens after a force-push/rebase or when RTD builds an older queued commit; triggering a rebuild on the latest PR head commit should fix it.

@acul71
Copy link
Copy Markdown
Contributor

acul71 commented Mar 28, 2026

@seetadev @Winter-Soren

I wanted to align on a few open points before we treat this as merge-ready—not as a blocker list, but so expectations are clear on both sides.

1. PR description checklist
The body still has unchecked items (commit history, docs, release notes). From what I see, a lot of that is already covered (e.g. newsfragment, docs). Can you confirm whether those checkboxes are simply outdated and should be refreshed, or whether something is still genuinely missing from your side?

2. _report_misbehaviour typing
In extensions.py, the misbehaviour callback is currently typed loosely (object). A small follow-up would be to narrow it to something like Callable[[ID], None] | None if the type checker is happy and there are no import cycles. Is that something you’re willing to fold in, or do you prefer to leave it for a follow-up PR?

3. Protobuf / Topic Observation vs other implementations
The rpc.proto layout looks consistent with the GossipSub v1.3 extensions story we care about, and the in-file notes help. Interop with every other libp2p stack is a separate question: Topic Observation and the extra control fields are partly experimental, and stock go-libp2p-pubsub / nim-libp2p won’t necessarily speak that dialect end-to-end. I’m fine with landing this as long as we’re explicit that cross-implementation Topic Observation interop isn’t guaranteed until everyone agrees on the same wire format (or we add targeted interop tests). Does that match your understanding, and are you comfortable stating that in the PR description or a short comment so reviewers don’t assume universal interop?

Merge readiness (not only the above)
Tidying the checklist, the optional typing tweak, and the wording on protobuf/interop helps, but by itself it isn’t the full definition of “ready to merge.” We still need green CI on the current head, no open required review threads, and maintainer sign-off—including comfort level with the experimental Topic Observation wire layout.

Thanks—happy to unblock once these are clarified.



Full review document: AI-PR-REVIEW-1237-1.md (click to expand) # AI PR Review: [#1237](https://github.com//pull/1237) — GossipSub v1.3 Extensions & Topic Observation

Branch reviewed: enh/1231-add-gossipsub-1.3-support (HEAD 4ea3b210 at time of review)
Review date: 2026-03-28
Prompt: downloads/py-libp2p-pr-review-prompt.md


1. Summary of Changes

This PR implements full GossipSub v1.3 Extensions Control Message behaviour and the Topic Observation extension, addressing #1231 (full v1.3 wire semantics, first-message rules, “at most once” enforcement, and observer IHAVE notifications).

Main areas:

Area Change
Protobuf rpc.proto updated with ControlExtensions (field 6), top-level TestExtension, ControlObserve / ControlUnobserve on ControlMessage; regenerated rpc_pb2.py / rpc_pb2.pyi.
Extensions state New libp2p/pubsub/extensions.py: PeerExtensions, ExtensionsState (hello injection, receive path, duplicate-Extensions misbehaviour), TopicObservationState.
PubSub _handle_new_peer() injects extensions only when the stream’s negotiated protocol is in _MESHSUB_V13_PLUS and the router exposes extensions_state + supports_v13_features.
GossipSub PROTOCOL_ID_V13, routing through extensions_state.handle_rpc, Topic Observation (handle_observe / handle_unobserve, emit_*, _notify_observers with hex message IDs), scorer penalty on duplicate Extensions.
RPC queue Splitting logic adjusted for optional ControlExtensions (commit ba224a8).
Tests New test_gossipsub_v1_3_extensions.py; updates to test_gossipsub_v14_extensions.py, test_rpc_queue.py.
Docs New docs/gossipsub-1.3.rst (lifecycle + example), index/examples wiring.

Breaking changes: None called out; protocol negotiation remains backward compatible for peers on older meshsub versions.


2. Branch Sync Status and Merge Conflicts

Branch sync status

  • git rev-list --left-right --count origin/main...HEAD: 0 33
  • Interpretation: The PR branch includes all of origin/main (0 commits behind) and adds 33 commits not yet on main (ahead).
  • Status: Up to date with main; no rebase urgency for sync alone.

Merge conflict analysis

  • Test merge: git merge --no-commit --no-ff origin/mainAlready up to date (exit 0).
  • Conflicts: None.
✅ **No merge conflicts detected.** The PR branch can be merged cleanly into origin/main.

3. Strengths

  • Spec alignment: ControlExtensions as a single message (field 6), experimental field numbers (e.g. 6492434 for test extension), and comments linking gossipsub-v1.3.md and extensions.proto match the documented v1.3 shape.
  • Interop documentation in-tree: rpc.proto explicitly notes that Topic Observation is not yet in upstream extensions.proto and points to the ethresearch proposal and go-libp2p alignment — this addresses maintainer concern about wire-format intent.
  • Stateful design: ExtensionsState mirrors go-libp2p’s pattern (sent/received extensions, misbehaviour callback), which should ease cross-implementation reasoning.
  • Hello path: Extensions are added only when negotiated_protocol in _MESHSUB_V13_PLUS — fixes the earlier “gate by negotiated protocol” review comment.
  • Observer IHAVE path: _notify_observers uses msg_id.hex() with an explicit comment tying it to safe_bytes_from_hex in handle_ihave — fixes the str(bytes) bug from review.
  • Tests: Large dedicated suite for v1.3 extensions; rpc_queue tests updated for the new control shape.
  • User-facing docs: docs/gossipsub-1.3.rst covers lifecycle (start_observing_topic / stop_observing_topic) and IHAVE-only semantics — addresses the “minor / follow-up” doc request from acul71’s review.
  • Newsfragment: newsfragments/1231.feature.rst is present, user-focused, and newline-terminated.

4. Issues Found

Critical

  • None identified in the current branch for core correctness paths reviewed (extensions hello, observer notifications, duplicate Extensions).

Major

  • Maintainer feedback — protobuf registry vs. Topic Observation: acul71 asked for clarity when Topic Observation field layout differs from published upstream schemas. The PR now documents this in rpc.proto (interop note + ethresearch link). Residual risk: other clients must share the same experimental layout for Topic Observation to interoperate; the PR description could still add one sentence pointing to the in-file note for reviewers who only read GitHub (not blocking if maintainers accept the proto comments).

Minor

  • PR body checklist: The PR description still lists unchecked items (“Clean up commit history”, “Add or update documentation…”, “Add entry to the release notes”). Documentation and newsfragments/1231.feature.rst largely satisfy user-facing release notes via Towncrier; the checklist is stale and should be updated so reviewers do not assume missing work.
  • Commit history: Many merge commits from main; not a code defect, but the author’s own TODO mentions cleanup — optional squash/rebase for history readability (maintainer preference).
- **File:** `libp2p/pubsub/extensions.py` (conceptual)
- **Line(s):** N/A
- **Issue:** `_report_misbehaviour` is typed loosely (`object`); works but reduces static assurance.
- **Suggestion:** Use `Callable[[ID], None] | None` if mypy/pyrefly allow without circular imports.

5. Security Review

  • Risk: Malicious peer sends repeated Extensions control messages.
    Impact: Low (peer scoring / penalty path).
    Mitigation: Implemented via _report_extensions_misbehaviourscorer.penalize_behavior.

  • Risk: OBSERVE / UNOBSERVE without advertising Topic Observation.
    Impact: Low (ignored or no extra data leaked).
    Mitigation: handle_observe checks peer_supports_topic_observation(sender_peer_id).

  • Risk: IHAVE message IDs to observers.
    Impact: Low — IDs are hex strings consistent with the rest of the stack; avoids parse failures from str(bytes).

  • Risk: Large or hostile protobuf control messages.
    Impact: Medium in general for pubsub; not introduced uniquely by this PR beyond additional control fields. Existing queue/size limits apply; no new unbounded allocation pattern was flagged in the reviewed diff.


6. Documentation and Examples

  • Docstrings: GossipSub methods (handle_observe, _notify_observers, etc.) are documented; Sphinx formatting was fixed in an earlier commit per CI.
  • Guide: docs/gossipsub-1.3.rst provides overview, lifecycle, and a code example.
  • Examples index: docs/examples.rst / docs/libp2p.pubsub.rst updated to surface the new page.
  • Gap: None material for merge; optional follow-up is a runnable example under examples/ if the project wants parity with other protocols (not required by the prompt’s minimum).

7. Newsfragment Requirement

  • Issue reference: #1231 is referenced in the PR body and issue text.
  • File: newsfragments/1231.feature.rstpresent, ReST bullet list, user-facing, ends with newline.
  • Verdict: Satisfies the mandatory newsfragment requirement for this issue.

8. Tests and Validation

Local runs on checked-out PR branch (source venv/bin/activate):

Command Result Notes
make lint Pass (exit 0) Pre-commit hooks including ruff, mdformat, path audit, mypy/pyrefly hooks — all passed.
make typecheck Pass (exit 0) mypy + pyrefly passed.
make test Pass (exit 0) 2605 passed, 15 skipped, ~99s.
make linux-docs Pass (exit 0) Sphinx -W build succeeded; gossipsub-1.3 included.

Test output warnings: 24× PytestCollectionWarning in tests/core/records/test_ipns_validator.py (TestVector / TypedDict) — pre-existing / unrelated to this PR.

GitHub Actions (gh pr checks 1237): At snapshot time, Read the Docs and many tox jobs reported pass; some core matrix jobs were still pending. Re-run or wait for green across Python versions before declaring CI fully green. Prior maintainer comments about CI referred to earlier failures; current local + RTD success is encouraging.


9. Recommendations for Improvement

  1. Update the PR description checklist to reflect completed items (newsfragment, docs page, hex/protocol fixes).
  2. After CI fully completes, paste a short “validation matrix” comment (Python versions × core) for maintainers.
  3. Optionally tighten ExtensionsState callback typing for maintainability.
  4. If squashing is desired, coordinate with maintainers before force-pushing (affects RTD “commit not found” issues after rebases).

10. Questions for the Author

  1. Interop testing: Has Topic Observation been exercised against go-libp2p or rust-libp2p with the same rpc.proto layout, or is this validated only within py-libp2p tests for now?
  2. Future upstream: If libp2p/specs adds Topic Observation to extensions.proto with different numbers, is the plan to version-gate or migrate in a follow-up PR?

11. Overall Assessment

  • Quality Rating: Good — Large, coherent feature with tests, docs, newsfragment, and prior review feedback addressed in code (hex ID, protocol gating, newsfragment, interop notes).
  • Security Impact: Low — Standard pubsub trust model; duplicate-Extensions and OBSERVE gating reduce abuse surface.
  • Merge Readiness: Ready from local validation (lint, typecheck, tests, docs) and a clean merge with main; confirm remaining pending CI matrix jobs on GitHub and maintainer acceptance of the experimental Topic Observation wire format documented in rpc.proto.
  • Confidence: High for the reviewed areas; Medium for cross-client Topic Observation until interop is explicitly confirmed.

Maintainer feedback tracking

Item (from acul71 / seetadev) Status
Newsfragment Done (1231.feature.rst)
Hex message IDs for observers Done (msg_id.hex() in _notify_observers)
Protocol gating for hello extensions Done (negotiated_protocol in _MESHSUB_V13_PLUS)
Protobuf / interop clarification Addressed in rpc.proto header + ethresearch / go reference
Docs example for Topic Observation Done (docs/gossipsub-1.3.rst)
CI green Verify pending GitHub jobs; local + RTD pass

12. Protobuf fix and cross-implementation comparison (nim / go / js)

12.1 What “the protobuf fix” means in py-libp2p (PR #1237)

The GossipSub v1.3 spec requires:

  • A single ControlExtensions message on ControlMessage (field 6), advertising which extensions the sender supports.
  • Optional top-level RPC fields for extension payloads (with experimental field numbers > 0x200000 where required).
  • Extensions only in the first RPC on the stream, at most once — that part is code (extensions.py, hello injection), but the wire has to match the .proto.

So the “fix” is not a one-line tweak: it is bringing rpc.proto in line with that wire layout, regenerating rpc_pb2.py / rpc_pb2.pyi, and then implementing behaviour on top.

Relevant excerpts from libp2p/pubsub/pb/rpc.proto:

message ControlMessage {
  repeated ControlIHave ihave = 1;
  repeated ControlIWant iwant = 2;
  repeated ControlGraft graft = 3;
  repeated ControlPrune prune = 4;
  repeated ControlIDontWant idontwant = 5;
  optional ControlExtensions extensions = 6;
  repeated ControlObserve observe = 7;
  repeated ControlUnobserve unobserve = 8;
}

message ControlExtensions {
  optional bool topicObservation = 1;
  optional bool testExtension = 6492434;
}

message RPC {
  // ...
  optional TestExtension testExtension = 6492434;
}

The file header explains the maintainer concern (Topic Observation not in upstream extensions.proto yet): document interop intent and field-number stability — that is the “protobuf / interop” fix in the review sense, not changing numbers after release.

12.2 go-libp2p (go-libp2p-pubsub)

The canonical Go schema is go-libp2p-pubsub pb/rpc.proto (pubsub lives in a separate module from the main go-libp2p repo).

It does have GossipSub v1.3–style pieces:

  • ControlMessage.extensions = 6ControlExtensions with e.g. partialMessages = 10, testExtension = 6492434.
  • Top-level RPC.testExtension = 6492434, plus PartialMessages extension types.

It does not include the Topic Observation observe / unobserve / topicObservation fields from py’s rpc.proto in the upstream master copy reviewed — those are extra in py, aligned with a reference / experimental layout (as the py comments say).

How Go does it: generate Go code from .proto, plus an extensions state machine in Go (same idea as extensions.py).

12.3 nim-libp2p

GossipSub 1.3 is present as a protocol id (GossipSubCodec_13 = "/meshsub/1.3.0" in libp2p/protocols/pubsub/gossipsub/types.nim).

They model ControlExtensions and wire encode/decode in Nim (e.g. libp2p/protocols/pubsub/rpc/messages.nim, protobuf.nim), with fields such as partialMessageExtension, testExtension, pingpongExtension, preambleExtension on ControlExtensions, and extensions on ControlMessage.

They also have tests under tests/libp2p/pubsub/extensions/ for “extensions not first message”, duplicate ControlExtensions, etc. — same family of rules as v1.3, with a different extension set than py’s Topic Observation–centric rpc.proto.

12.4 js-libp2p (@libp2p/gossipsub)

No GossipSub 1.3 protocol in the sense of /meshsub/1.3.0: constants.ts stops at /meshsub/1.2.0, and gossipsub.ts registers [GossipsubIDv12, GossipsubIDv11, GossipsubIDv10].

The generated ControlMessage codec in packages/gossipsub/src/message/rpc.ts only encodes/decodes fields 1–5 (ihave / iwant / graft / prune / idontwant) — no field 6 extensions.

So js-libp2p does not implement GossipSub v1.3 Extensions on the wire in this package today.

12.5 Side-by-side

Implementation /meshsub/1.3.0 ControlMessage.extensions (field 6) Topic Observation Notes
py-libp2p (PR) Yes Yes Yes (custom fields + doc) Matches spec shape + experimental TO; TestExtension 6492434
go-libp2p-pubsub (negotiated via pubsub) Yes Not in upstream rpc.proto cited Reference for partial messages + testExtension
nim-libp2p Yes Yes (in Nim types) Not the same as py Other extensions (partial, ping-pong, …)
js-libp2p No No (codec) No Stops at v1.2

13. Interop with Nim and Go (does this protobuf setup work?)

Short answer

Partially. The parts that match the shared v1.3 / go rpc.proto story are the ones that can interoperate with Go and Nim in a meaningful way. The Topic Observation and py-specific ControlExtensions fields are not something you should assume work end-to-end with stock go-libp2p-pubsub or nim-libp2p unless they implement the same experimental wire layout (or only ignore it).

What actually lines up

  1. ControlMessage.extensions (field 6) — Go has this; Nim has extensions on the control message. So the idea of v1.3 (“extensions blob on control”) is aligned.

  2. testExtension = 6492434 on ControlExtensions and the empty TestExtension on RPC — Go uses the same kind of registration for the interop test extension. Py does too. That path is the right one to say: “does the v1.3 extension plumbing interoperate?” — yes, in principle, with Go, for that test extension, if both negotiate a 1.3-capable meshsub and send compatible RPCs.

  3. Unknown fields — Protobuf implementations are expected to ignore unknown fields. So if py sends extra fields (Topic Observation) and the other side does not define them, the usual outcome is: they are ignored, not a decode failure — unless something in the stack assumes “only known fields” in a non-standard way (unusual).

Where it does not “just work”

With Go (go-libp2p-pubsub)

  • Upstream Go ControlExtensions uses things like partialMessages = 10 and testExtension = 6492434, not the same set of fields as py’s topicObservation = 1 on ControlExtensions.
  • Go’s rpc.proto (as typically shipped) does not define py’s observe / unobserve on ControlMessage (fields 7 / 8).
  • So:
    • GossipSub at /meshsub/1.3.0 + generic pubsub + ControlExtensions + test extension: reasonable to expect interop for what Go actually implements.
    • Topic Observation (OBSERVE/UNOBSERVE, topicObservation flag, immediate IHAVE to observers): not something you can assume works with vanilla Go until Go implements the same experimental schema (or you run a fork that does).

With Nim

  • Nim has /meshsub/1.3.0 and ControlExtensions, but their extension flags are oriented around partial message / ping-pong / preamble-style features, not py’s Topic Observation set.
  • Same story: shared mechanism (field 6, rules about first message / duplicates) can conceptually align; feature-by-feature interop requires matching definitions for each extension you care about.

Practical takeaway

Goal Py ↔ Go Py ↔ Nim
“We’re on meshsub 1.3+ and exchange normal GossipSub” Yes, if negotiated Yes, if negotiated
“v1.3 ControlExtensions + testExtension interop” Plausible (same field numbers as Go for that bit) Depends on Nim’s encoding of the same fields
Topic Observation as implemented in py” Do not assume without matching Go code/proto Do not assume without matching Nim code

So the concern is valid: this protobuf setup is “correct” for py and for the spec-shaped core, but Topic Observation + py’s extra control fields are an explicit experimental / cross-client agreement, not something guaranteed with arbitrary Nim/Go builds. For real confidence, run interop tests against a specific Go commit and Nim commit (or document “py-only” / “py + fork X”).

One sentence for a PR: Interop with Go/Nim on base v1.3 extensions is plausible; Topic Observation requires the same experimental wire definitions on both sides or peers that only ignore unknown fields without breaking higher-level logic.


Artifacts: downloads/AI-PR-REVIEWS/1237/lint_output.log, typecheck_output.log, test_output.log, docs_output.log, branch_sync_status.txt, merge_conflict_check.log (regenerated for this review).

@Winter-Soren
Copy link
Copy Markdown
Contributor Author

Thank you @acul71 for the comprehensive review

1. Checklist - Those items are stale. Docs are in docs/gossipsub-1.3.rst, and user-facing release notes are covered by newsfragments/1231.feature.rst (Towncrier). I’ll refresh the PR description so it doesn’t look like open work. Commit-history cleanup is optional, and I’m happy to follow the maintainer's preference (merge vs squash).

2. Typing - I’ve narrowed ExtensionsState’s misbehaviour callback to Callable[[ID], None] | None in extensions.py (pushed in the latest commit).

3. Interop - Agreed: base GossipSub v1.3 extension mechanics are what we’re aligning with the spec and with other implementations where they support /meshsub/1.3.0 and ControlExtensions; Topic Observation and the extra observe/unobserve wiring are not guaranteed to interoperate with stock go-libp2p-pubsub / nim-libp2p / js until they implement the same experimental layout (or we add targeted interop tests). I’ve added an explicit Interop expectations paragraph to the PR description and pointed reviewers at the notes in rpc.proto.

Merge readiness - Understood: green CI on the current head, resolved review threads, and maintainer sign-off remain the bar.

@seetadev
Copy link
Copy Markdown
Contributor

@Winter-Soren : Hi Soham. Appreciate the thorough and thoughtful follow-up here. The clarifications around the checklist and documentation help remove any ambiguity on PR completeness, and the update to typing for ExtensionsState makes the intent much clearer and safer.

The explicit callout on interop expectations is especially valuable, it sets the right context for reviewers and downstream implementers, particularly given the experimental nature of Topic Observation and related wiring. Pointing to rpc.proto and documenting divergence from other libp2p stacks is exactly the kind of transparency we want at this stage.

Overall, this feels well-aligned with the GossipSub v1.3 spec, and the PR is in a good state from a maintainability and reviewability standpoint. Thanks for proactively addressing feedback and tightening the edges.

Please share a detailed discussion page sharing demo screencasts and implementation improvements, steps.

Looks ready for merge. CCing @acul71 and @pacrob

@pacrob
Copy link
Copy Markdown
Member

pacrob commented Mar 30, 2026

Please make sure the demo code in the docs works. Also there's a GossipSub v1.3 TODO at line 2330 in gossipsub.py - can that be removed now?

Once these are handled I can approve for merge.

@Winter-Soren
Copy link
Copy Markdown
Contributor Author

Please make sure the demo code in the docs works. Also there's a GossipSub v1.3 TODO at line 2330 in gossipsub.py - can that be removed now?

Once these are handled I can approve for merge.

Thank you so much @pacrob for the review!!
I have removed the TODO at line 2330 since the task has been implemented. Regarding the demo, I have presented it as a general usage example rather than a working one. The working demo is included in PR #1139.

@Winter-Soren Winter-Soren requested a review from pacrob March 31, 2026 10:09
Copy link
Copy Markdown
Member

@pacrob pacrob left a comment

Choose a reason for hiding this comment

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

lgtm!

@seetadev seetadev merged commit aa542b1 into libp2p:main Mar 31, 2026
38 checks passed
@Winter-Soren
Copy link
Copy Markdown
Contributor Author

You can find the working demonstration of Gossipsub v1.3 here

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.

5 participants