Skip to content

fix(p2p): frame gossipsub msgId and restrict allowedTopics (A-1256)#24214

Draft
PhilWindle wants to merge 1 commit into
merge-train/spartan-v5from
phil/a-1256-p2pgossipsub-unframed-topicdata-msgid-lets-arbitrary-topics
Draft

fix(p2p): frame gossipsub msgId and restrict allowedTopics (A-1256)#24214
PhilWindle wants to merge 1 commit into
merge-train/spartan-v5from
phil/a-1256-p2pgossipsub-unframed-topicdata-msgid-lets-arbitrary-topics

Conversation

@PhilWindle

Copy link
Copy Markdown
Collaborator

Fixes A-1256.

Problem

The gossipsub full message id was SHA256(topic || data)[0..20] with no framing between the topic string and the message bytes, and LibP2PService set no allowedTopics. Raw concatenation isn't injective: for a real message (T, D), a peer can craft T' = T + D[0], D' = D[1:] so T' || D' is byte-identical to T || D → same msgId. ChainSafe gossipsub transforms the arbitrary-topic message and inserts that id into seenCache before the subscription check (it isn't delivered to us, since we're not subscribed to T'). When the genuine proposal/attestation arrives on T, gossipsub drops it as a duplicate before application validation, peer scoring, or handling — suppressing a time-sensitive consensus message within the slot.

Fix (defense in depth — either alone breaks the attack)

  1. Frame the msgId input as uint32be(topicLen) || topic || data (encoding.ts). The topic length pins the (topic, data) boundary, so a boundary-shifted pair no longer collides. (getMsgIdFn's parameter is narrowed to Pick<Message, 'topic' | 'data'> — the only fields it reads — which stays assignable to gossipsub's msgIdFn slot.)
  2. Set exact allowedTopics (libp2p_service.ts) to the subscribed Aztec topic strings. Verified against the installed gossipsub: the allowlist is enforced in handleReceivedRpc before handleReceivedMessage/seenCache.put, so an unsubscribed-topic message is dropped before transform / msgId / seenCache.

Test

encoding.test.ts: builds a real P2PMessage.toMessageData() buffer (confirming data[0] === 0x00), constructs the shifted (T', D'), and asserts the two msg ids now differ (they collided before the framing change); plus a determinism check.

Compatibility

msgId is computed locally for dedup; changing the function doesn't change any on-wire format. During a rolling upgrade, mixed nodes briefly compute different ids for the same message (minor IHAVE/IWANT inefficiency), with no correctness impact.

The gossipsub msgId was SHA256(topic || data) with no framing between the
topic string and the message bytes, and no allowedTopics was set. A peer
could publish on an unsubscribed topic T+data[0] with data[1:], whose
(topic, data) concatenation is byte-identical to a real message on topic
T, hashing to the same msgId. gossipsub transformed it and inserted the
id into seenCache before the subscription check, so the genuine
proposal/attestation was later dropped as a duplicate, suppressing a
time-sensitive consensus message.

- Frame the topic length into the msgId input (uint32be(topicLen) ||
  topic || data) so the (topic, data) boundary is unambiguous and a
  boundary-shifted pair no longer collides.
- Set exact allowedTopics on the gossipsub config so an unsubscribed-topic
  message is rejected before transform / msgId / seenCache insertion.

Defense in depth: either fix alone breaks the attack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@PhilWindle PhilWindle added the ci-draft Run CI on draft PRs. label Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-draft Run CI on draft PRs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant