Skip to content

Conversation

@yiweichi
Copy link
Member

@yiweichi yiweichi commented Jan 12, 2026

Problem

Malicious peers can DoS the node by sending old blocks.

Solution

Two-layer defense:

  1. Finalized Block Protection
    if block_number <= finalized_height { penalize(peer); // Immediately reject}
  2. Duplicate Block Detection
    // Cache recent blocks per peerif peer_already_sent_this_block { penalize(peer); // DoS detected }

Challenge

Multi-Protocol Handling
Problem: Nodes use both scroll-wire and eth-wire protocols. Same peer may legitimately send same block via both protocols.
Solution: Track duplicates per protocol

struct PeerBlockState {
    scroll_wire_received: LruCache<B256>,  // scroll-wire duplicates
    eth_wire_received: LruCache<B256>,     // eth-wire duplicates
}

// Only penalize duplicates within SAME protocol
if peer_sent_via_scroll_wire && receives_again_via_scroll_wire {
    penalize();  // DoS attack
}

if peer_sent_via_scroll_wire && receives_via_eth_wire {
    accept();    // Different protocol, OK
}

Fixes #307

@codspeed-hq
Copy link

codspeed-hq bot commented Jan 12, 2026

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing feat-penalize-peer-on-duplicate-block (9ccf17a) with main (2ae9352)

Open in CodSpeed

@yiweichi yiweichi changed the title feat: penalize peer on duplicate block feat: handle DoS node by sending block Jan 19, 2026
@yiweichi yiweichi requested a review from frisitano January 19, 2026 08:46
Copy link
Collaborator

@frisitano frisitano left a comment

Choose a reason for hiding this comment

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

Looks good. Added a few comments inline. Can you add some test cases, please?

I'm also wondering if we can simplify the logic a bit. The idea would be that if we have a connection with a peer over scroll-wire, then we should only gossip on scroll-wire. If we don't have a scroll-wire connection, then we use eth-wire. I think this should allow us to consolidate the block cache per peer into a single cache.

} else {
None
}
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if we should change the logic below.

If we have a connection with a peer via scroll wire - only send via scroll wire. If we don't have a connection with a peer via scroll wire then send only via eth wire.

What do you think?

Comment on lines +258 to +259
// Check if this peer has already received this block via scroll-wire, if so
// penalize it.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Check if this peer has already received this block via scroll-wire, if so
// penalize it.
// Check if we have already received this block via scroll-wire from this peer, if so
// penalize it.

Comment on lines -246 to +285
// Update the state of the peer cache i.e. peer has seen this block.
// Update the state: peer has seen this block via scroll-wire
self.scroll_wire
.state_mut()
.peer_block_state_mut()
.entry(peer_id)
.or_insert_with(|| LruCache::new(LRU_CACHE_SIZE))
.insert(block_hash);
.or_insert_with(|| PeerBlockState::new(LRU_CACHE_SIZE))
.insert_scroll_wire(block_hash);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't we already do this above on line 274 above?

{
let block_hash = block.hash_slow();

// Check if this peer has already sent this block to us via eth-wire, if so penalize it.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Check if this peer has already sent this block to us via eth-wire, if so penalize it.
// Check if we have already received this block from this peer via eth-wire, if so, penalize the peer.

Comment on lines 405 to +409
self.scroll_wire
.state_mut()
.peer_block_state_mut()
.entry(peer_id)
.or_insert_with(|| LruCache::new(LRU_CACHE_SIZE))
.insert(block_hash);
.or_insert_with(|| PeerBlockState::new(LRU_CACHE_SIZE))
.insert_eth_wire(block_hash);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't we already do this above on line 396?

/// is just a cache of the last 100 blocks seen by each peer.
state: HashMap<PeerId, LruCache<B256>>,
/// Unified state tracking block state and blocks received from each peer via both protocols.
peer_block_state: HashMap<PeerId, PeerBlockState>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I propose we rename this to peer_state and PeerBlockState to PeerState

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.

[Network] Penalize peer if they send us duplicate block

3 participants