Skip to content

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

Merged
PhilWindle merged 3 commits into
merge-train/spartan-v5from
phil/a-1256-p2pgossipsub-unframed-topicdata-msgid-lets-arbitrary-topics
Jun 22, 2026
Merged

PhilWindle merged 3 commits into
merge-train/spartan-v5from
phil/a-1256-p2pgossipsub-unframed-topicdata-msgid-lets-arbitrary-topics

Conversation

@PhilWindle

Copy link
Copy Markdown
Contributor

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
@PhilWindle
PhilWindle marked this pull request as ready for review June 22, 2026 11:58
*/
export async function getMsgIdFn({ topic, data }: Message): Promise<Uint8Array> {
const buffer = Buffer.concat([Buffer.from(topic), data]);
export async function getMsgIdFn({ topic, data }: Pick<Message, 'topic' | 'data'>): Promise<Uint8Array> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are any changes needed to fastMsgIdFn or msgIdToStrFn above?

I'm a bit unclear on when those are used (and why it's called fast, since I think using webcrypto.subtle is faster)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are right. Though I actually think the best course of action is to just remove the fastMsgId. It's an optional optimisation that actually makes the message caching non-collision resistant.

PhilWindle and others added 2 commits June 22, 2026 15:11
…(A-1256)

The gossipsub fast-path dedup cache keyed on fastMsgIdFn — a
non-cryptographic 64-bit xxhash of the raw data only (no topic), seeded
from Math.random (~2^30, non-CSPRNG). A fast-id collision (accidental, or
engineered given the weak seed, or simply same-data-on-a-different-topic)
makes gossipsub drop a different message as a duplicate with no fallback
to the full id, so it's a gossip-suppression vector that the framed full
msgId does not close (the fast path short-circuits before it).

fastMsgIdFn is optional; removing it leaves dedup resting solely on the
cryptographic, topic-framed msgIdFn (SHA-256). Also removes the now-unused
xxhash machinery from encoding.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No longer used after removing fastMsgIdFn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@PhilWindle
PhilWindle merged commit ac59fe0 into merge-train/spartan-v5 Jun 22, 2026
12 checks passed
@PhilWindle
PhilWindle deleted the phil/a-1256-p2pgossipsub-unframed-topicdata-msgid-lets-arbitrary-topics branch June 22, 2026 15:53
rangozd pushed a commit to rangozd/aztec-packages that referenced this pull request Aug 5, 2026
BEGIN_COMMIT_OVERRIDE
fix(p2p): re-seed discovery from persisted peer ENRs after restart
(AztecProtocol#24169)
docs(e2e): annotate e2e tests with setup/category notes (AztecProtocol#24191)
chore: merge v5-next into merge-train/spartan-v5 (raw, conflict markers)
(AztecProtocol#24221)
fix(archiver): index zero-field logs under empty tag instead of throwing
(A-1253) (AztecProtocol#24212)
fix(prover-node): report awaiting-root and publishing-proof phases in
EpochSession (A-1212) (AztecProtocol#24216)
fix(p2p): bound declared contract-class bytecode length before
allocating (A-1258) (AztecProtocol#24213)
fix(p2p): frame gossipsub msgId and restrict allowedTopics (A-1256)
(AztecProtocol#24214)
chore: merge v5-next into merge-train/spartan-v5 (AztecProtocol#24226)
fix(p2p): guard ENR address parsing against malformed TCP fields
(A-1255) (AztecProtocol#24215)
END_COMMIT_OVERRIDE
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.

2 participants