For the complete documentation index, see llms.txt. This page is also available as Markdown.

75410 bc high cross topic message id collision in gossipsub allows attacker to censor blocks from network nodes

Submitted on Apr 29th 2026 at 01:35:05 UTC by @DeltaXV for Audit Comp | Base Azul

  • Report ID: #75410

  • Report Type: Blockchain/DLT

  • Report severity: High

  • Target: https://github.com/base/base/tree/v0.8.0-rc.28

  • Impacts:

    • Unintended chain split (network partition)

    • Temporary freezing of network transactions by delaying one block by 500% or more of the average block time of the preceding 24 hours beyond standard difficulty adjustments

Description

Brief/Intro

Base's gossipsub compute_message_id function computes message IDs from SHA256(domain || data) without including the topic. This follows the L1-inherited spec, which was safe on L1 where each topic carries a distinct payload type. However, Base's multi-version block topic architecture (V1 through V4) creates a situation the L1 spec never anticipated: the sequencer publishes identical compressed wire bytes that get routed to different SSZ decoders based on the topic. Because gossipsub maintains a single global duplicate_cache keyed by message ID, and inserts the ID before the application validator runs, an attacker can suppress legitimate blocks by republishing the sequencer's block data on a wrong-version topic. The wrong-topic copy fails SSZ decoding and is rejected, but the message ID is already cached — so the legitimate copy is silently dropped as a duplicate when it arrives. The victim node never receives the block. The attack can be sustained per-block, freezing the victim's unsafe head indefinitely until L1 derivation catches up (minutes to tens of minutes).

Vulnerability Details

Root Cause

The function compute_message_id in base/crates/consensus/gossip/src/config.rs:104 computes:

The hash is SHA256(domain || decompressed_data)[:20]. The msg.topic is not included.

This follows the L1 Ethereum consensus spec which uses the same formula. On L1, this is safe because each gossip topic carries a fundamentally different payload type (beacon blocks, attestations, etc.) — identical bytes across topics are practically impossible.

Base's architecture breaks this assumption. Base subscribes to four block topics simultaneously (V1 through V4), where the wire format is nearly identical across versions. Specifically, V4 data is V3 data with an appended withdrawals_root field — the signature, parent beacon root, and compressed SSZ payload prefix are byte-for-byte identical. This means the sequencer's V4 block, re-published on the V3 topic, produces the exact same message ID. gossipsub's global duplicate_cache then treats the two as duplicates.

Why This Is Exploitable

gossipsub (libp2p-gossipsub v0.49.4, used by Base) maintains a single global duplicate_cache that is not per-topic. When a message is received, the message ID is inserted into this cache before the application-level validator runs. The relevant code in libp2p-gossipsub/src/behaviour.rs:1781:

The duplicate_cache is write-only — there is no remove method anywhere in the gossipsub source. Once a message ID is inserted, it remains for duplicate_cache_time (120 seconds in Base's config at config.rs:89). A Reject result from the application validator removes the message from mcache but not from duplicate_cache.

Attack Path

1

Attacker joins the mesh

The attacker joins the gossipsub mesh as a regular peer (any libp2p node can connect — no authentication beyond the Noise XX handshake). The attacker subscribes to both the V3 topic (/optimism/{chain_id}/2/blocks) and V4 topic (/optimism/{chain_id}/3/blocks).

2

Sequencer publishes a block

The sequencer produces a block and publishes it as V4-encoded, snappy-compressed bytes on the V4 topic.

3

Attacker republishes the same bytes on V3

The attacker receives the block from the mesh and immediately re-publishes the exact same compressed bytes on the V3 topic to the victim node. If the attacker has lower network latency to the victim than the sequencer does (e.g. geographically closer, or the sequencer is multiple mesh hops away), the V3 copy arrives first.

4

Victim caches the wrong-topic message ID

The victim's gossipsub layer receives the V3 message:

  • Computes msg_id = SHA256([0x01,0,0,0] || decompress(data))[:20]

  • message_is_valid() passes (the attacker is not blacklisted)

  • duplicate_cache.insert(msg_id)inserts the ID (point of no return)

  • Delivers Event::Message to BlockHandler::handle()

  • Handler dispatches to decode_v3() because the topic is V3

  • ExecutionPayloadV3::from_ssz_bytes() fails on V4-encoded data (V4 has an additional withdrawals_root field that changes the SSZ offset table)

  • Handler returns (MessageAcceptance::Reject, None)

  • report_message_validation_result(Reject) removes from mcache, but not from duplicate_cache

5

Legitimate V4 block is dropped

The legitimate V4 message arrives at the victim:

  • gossipsub computes msg_id = SHA256([0x01,0,0,0] || decompress(data))[:20]identical to step 4 (same data, topic not in hash)

  • duplicate_cache.insert(msg_id) returns false → message is silently dropped

  • The victim's BlockHandler never sees the valid V4 block

6

The attack is repeated per block

The attacker repeats for every block. Each suppressed block's ID stays in the cache for 120 seconds (duplicate_cache_time). The victim's unsafe head freezes.

References

https://gist.github.com/DeltaXV/5d9371841dd53439b20cdd431879088b

Proof of Concept

The PoC consists of two tests added directly to the Base codebase, using the existing test infrastructure. Gist with all tests as well -> https://gist.github.com/DeltaXV/5d9371841dd53439b20cdd431879088b

Within the base/crates/consensus/gossip/src/config.rs test suite paste the following test at the end of the file:

And do the same for base/crates/consensus/gossip/src/handler.rs:

How to run

Test output

Both tests passing confirms the vulnerability: the assert_eq on message IDs proves the collision, and the handler test proves a valid V4 block is accepted on V4 but rejected on V3, with identical message IDs in both cases.

Mitigation

Include the topic string and its length in the message ID hash. This goes beyond the L1-inherited spec but is necessary for Base's multi-version topic architecture where identical wire bytes can appear on different topics:

Was this helpful?