Skip to content

feat(p2p): hard-reject handshakes from a different chain_id#1435

Merged
JesseTheRobot merged 2 commits into
masterfrom
fix/chain-id-mismatch
Jun 1, 2026
Merged

feat(p2p): hard-reject handshakes from a different chain_id#1435
JesseTheRobot merged 2 commits into
masterfrom
fix/chain-id-mismatch

Conversation

@JesseTheRobot

@JesseTheRobot JesseTheRobot commented Jun 1, 2026

Copy link
Copy Markdown
Member

What

A handshake declaring a different chain_id was previously accepted with only an advisory consensus-config-hash mismatch log. This makes the chain-ID mismatch a hard handshake rejection — we never peer with a node on a different chain. The consensus-config-hash mismatch stays advisory, as before.

How

  • Server enforcement (server.rs): both the V1 and V2 handshake handlers reject when version_request.chain_id != our_chain_id, checked before signature verification as a cheap network-membership gate (like the existing protocol-version check). Logs ours/theirs for operators.
  • New wire reason (types.rs): a dedicated RejectionReason::ChainIdMismatch, kept distinct from ProtocolMismatch (a chain mismatch isn't a protocol mismatch) and from the advisory consensus-config-hash check. It serializes as the bare unit string {"Rejected":"ChainIdMismatch"} via the existing v1-compat shim.
  • Client handling (gossip_client.rs): when we are rejected with ChainIdMismatch during our own handshake, it maps to a terminal NetworkMismatch rejection rather than a retryable request error — so we stop announcing to a cross-chain peer instead of retry-storming it.

Backwards compatibility (verified against the Dec-2025 source)

The variant only ever goes on the wire when the peer declared a different chain_id. A same-chain peer running an older build (e.g. the Dec-2025 V1 endpoint, whose derived deserializer doesn't know the string) declares our chain_id, so the new check is a pass-through no-op and it flows through the identical path as before — it never receives the new variant. The only peers that see ChainIdMismatch are ones we're deliberately refusing because they're on a different chain.

Test plan

  • p2p serde / fixture / coverage tests + proptest (rejection_reason_serializes_to_v1_wire_form, test_all_rejection_reasons_covered) — pass; fixtures/gossip_fixtures.json regenerated
  • peer_discovery integration test extended with a validly-signed handshake declaring a foreign chain_id, asserting ChainIdMismatch — pass (real V2 handler, end-to-end)
  • cargo fmt, cargo clippy on irys-p2p and irys-chain-tests — clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Peers with a mismatched chain ID are now immediately rejected during handshakes and labeled as "chain_id_mismatch", producing a clear "Chain ID mismatch — peer is on a different network" rejection.
  • Tests

    • Added tests and fixtures covering chain ID mismatch handling, including validation order (chain ID checked before signature verification) and telemetry/rejection labeling.

Previously a handshake declaring a different chain_id was accepted with
only an advisory consensus-config-hash mismatch log. Now both the V1 and
V2 handshake handlers hard-reject when the peer's chain_id differs from
ours, checked before signature verification as a cheap network-membership
gate. The consensus-config-hash mismatch stays advisory.

Adds a dedicated RejectionReason::ChainIdMismatch variant, kept distinct
from ProtocolMismatch (a chain mismatch is not a protocol mismatch) and
from the advisory config-hash check. It serializes as a bare unit string
for v1 wire compatibility and is only ever emitted to peers that declared
a different chain_id, so same-chain peers running older (e.g. Dec-2025)
builds never receive it and are unaffected.

When we are rejected with ChainIdMismatch during our own handshake it maps
to a terminal NetworkMismatch rejection rather than a retryable request
error, so we stop announcing to cross-chain peers instead of retrying.
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3d698273-2ba0-4191-8aee-fb9a61bdce97

📥 Commits

Reviewing files that changed from the base of the PR and between 4d0d451 and 5037ddc.

📒 Files selected for processing (1)
  • crates/chain-tests/src/multi_node/peer_discovery.rs

📝 Walkthrough

Walkthrough

This PR adds chain ID mismatch rejection to the gossip handshake protocol. A new RejectionReason::ChainIdMismatch variant is introduced with full serialization support. The server rejects V1 and V2 handshakes when the peer's chain ID does not match the configured chain ID. The client treats these rejections as terminal and propagates them through data request and pledge whitelist flows. Fixture and integration tests validate the new rejection reason.

Changes

Chain ID Mismatch Handshake Rejection

Layer / File(s) Summary
RejectionReason type and serialization contract
crates/p2p/src/types.rs, fixtures/gossip_fixtures.json
RejectionReason::ChainIdMismatch variant is introduced with serde implementations for both v1 unit and v2 object forms, proptest generation, and wire-form tests. Fixture JSON adds corresponding test data.
Server-side handshake chain ID validation
crates/p2p/src/server.rs
V1 and V2 handshake handlers add chain ID equality checks after protocol-version validation but before signature verification; on mismatch, they log a warning and return ChainIdMismatch rejection.
Client-side chain ID mismatch handling
crates/p2p/src/gossip_client.rs
Post-handshake flows treat ChainIdMismatch as a terminal rejection with NetworkMismatch response and no-retry semantics. Telemetry label mapping added for chain_id_mismatch.
Error propagation through request flows
crates/p2p/src/gossip_client.rs, crates/p2p/src/peer_network_service.rs
Chain ID mismatch is routed alongside protocol mismatch as a terminal non-retried failure in pull_data_from_network, stake_and_pledge_whitelist, and request_data_from_network_task.
Fixture and integration test coverage
crates/p2p/src/gossip_fixture_tests.rs, fixtures/gossip_fixtures.json, crates/chain-tests/src/multi_node/peer_discovery.rs
Fixture test entry and exhaustive coverage assertion for ChainIdMismatch variant. Integration test verifies end-to-end rejection of handshake with mismatched chain ID and also verifies chain-id check occurs before signature verification (invalid-signature variant).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • glottologist
  • DanMacDonald
  • roberts-pumpurs
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding hard rejection of handshakes from peers with mismatched chain_id. It is concise, descriptive, and directly maps to the primary objective across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chain-id-mismatch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/p2p/src/types.rs (1)

754-768: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add ChainIdMismatch to the v1/v2 consistency test.

The test validates that non-HandshakeRequired variants serialize identically through both envelopes. ChainIdMismatch is a unit variant and should be included to prevent future regressions.

✅ Proposed fix
     fn rejected_v2_json_matches_v1_for_non_handshake_variants() {
         for reason in [
             RejectionReason::GossipDisabled,
             RejectionReason::InvalidData,
             RejectionReason::RateLimited,
             RejectionReason::UnableToVerifyOrigin,
             RejectionReason::InvalidCredentials,
             RejectionReason::ProtocolMismatch,
             RejectionReason::UnsupportedFeature,
+            RejectionReason::ChainIdMismatch,
             RejectionReason::UnsupportedProtocolVersion(7),
         ] {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/p2p/src/types.rs` around lines 754 - 768, The test
rejected_v2_json_matches_v1_for_non_handshake_variants is missing the
RejectionReason::ChainIdMismatch unit variant; update the array of reasons in
that test to include RejectionReason::ChainIdMismatch so
GossipResponse::<()>::Rejected(reason) serialization via serde_json::to_value
still matches rejected_v2_json(reason) for that case and prevents future
regressions.
crates/p2p/src/gossip_client.rs (1)

959-969: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle ChainIdMismatch as unhealthy in check_health.

With the new rejection reason, this branch currently falls into _ and still returns Ok(true), so different-network peers can be kept online/eligible. Treat RejectionReason::ChainIdMismatch as terminal unhealthy (same class as network/protocol mismatch handling intent).

Suggested fix
-                    RejectionReason::InvalidCredentials | RejectionReason::ProtocolMismatch => {
+                    RejectionReason::InvalidCredentials
+                    | RejectionReason::ProtocolMismatch
+                    | RejectionReason::ChainIdMismatch => {
                         warn!("Health check rejected with reason: {:?}", reason);
+                        return Ok(false);
                     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/p2p/src/gossip_client.rs` around lines 959 - 969, The health check
currently treats RejectionReason::ChainIdMismatch as non-terminal because it
falls into the wildcard arm and still returns Ok(true); in the check_health
function update the match on RejectionReason so that
RejectionReason::ChainIdMismatch is handled the same as
RejectionReason::InvalidCredentials and RejectionReason::ProtocolMismatch (log a
warning and return Ok(false)) while keeping all other cases returning Ok(true).
Reference the match on RejectionReason inside check_health and add
ChainIdMismatch to that branch so peers from a different chain are marked
unhealthy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/chain-tests/src/multi_node/peer_discovery.rs`:
- Around line 138-183: Add a second handshake test case that proves chain_id is
checked before signature verification: clone the existing foreign_chain_request
(HandshakeRequestV2) into a new variable (e.g., foreign_chain_bad_sig), corrupt
its signature field (or set to Default::default()), send it via the same
TestRequest to GossipRoutes::Handshake through
call_service/read_body/serde_json, and assert the response is
GossipResponse::Rejected with RejectionReason::ChainIdMismatch (not
InvalidCredentials); this ensures the chain_id check wins even when the
signature is invalid.

---

Outside diff comments:
In `@crates/p2p/src/gossip_client.rs`:
- Around line 959-969: The health check currently treats
RejectionReason::ChainIdMismatch as non-terminal because it falls into the
wildcard arm and still returns Ok(true); in the check_health function update the
match on RejectionReason so that RejectionReason::ChainIdMismatch is handled the
same as RejectionReason::InvalidCredentials and
RejectionReason::ProtocolMismatch (log a warning and return Ok(false)) while
keeping all other cases returning Ok(true). Reference the match on
RejectionReason inside check_health and add ChainIdMismatch to that branch so
peers from a different chain are marked unhealthy.

In `@crates/p2p/src/types.rs`:
- Around line 754-768: The test
rejected_v2_json_matches_v1_for_non_handshake_variants is missing the
RejectionReason::ChainIdMismatch unit variant; update the array of reasons in
that test to include RejectionReason::ChainIdMismatch so
GossipResponse::<()>::Rejected(reason) serialization via serde_json::to_value
still matches rejected_v2_json(reason) for that case and prevents future
regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c8f4be62-289a-499d-a080-e27286da653d

📥 Commits

Reviewing files that changed from the base of the PR and between a045ba9 and 4d0d451.

📒 Files selected for processing (7)
  • crates/chain-tests/src/multi_node/peer_discovery.rs
  • crates/p2p/src/gossip_client.rs
  • crates/p2p/src/gossip_fixture_tests.rs
  • crates/p2p/src/peer_network_service.rs
  • crates/p2p/src/server.rs
  • crates/p2p/src/types.rs
  • fixtures/gossip_fixtures.json

Comment thread crates/chain-tests/src/multi_node/peer_discovery.rs
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Add a handshake case with a foreign chain_id and a zeroed (invalid)
signature, asserting the rejection is ChainIdMismatch rather than
InvalidCredentials. Guards against ever reordering the chain_id check
after signature verification.
@JesseTheRobot JesseTheRobot merged commit 12614c1 into master Jun 1, 2026
18 checks passed
@JesseTheRobot JesseTheRobot deleted the fix/chain-id-mismatch branch June 1, 2026 21:24
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.

1 participant