feat(p2p): hard-reject handshakes from a different chain_id#1435
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR adds chain ID mismatch rejection to the gossip handshake protocol. A new ChangesChain ID Mismatch Handshake Rejection
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 winAdd
ChainIdMismatchto the v1/v2 consistency test.The test validates that non-
HandshakeRequiredvariants serialize identically through both envelopes.ChainIdMismatchis 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 winHandle
ChainIdMismatchas unhealthy incheck_health.With the new rejection reason, this branch currently falls into
_and still returnsOk(true), so different-network peers can be kept online/eligible. TreatRejectionReason::ChainIdMismatchas 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
📒 Files selected for processing (7)
crates/chain-tests/src/multi_node/peer_discovery.rscrates/p2p/src/gossip_client.rscrates/p2p/src/gossip_fixture_tests.rscrates/p2p/src/peer_network_service.rscrates/p2p/src/server.rscrates/p2p/src/types.rsfixtures/gossip_fixtures.json
|
Benchmark results: https://irys-xyz.github.io/irys/dev/bench/fix%2Fchain-id-mismatch/index.html |
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.
What
A handshake declaring a different
chain_idwas 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.rs): both the V1 and V2 handshake handlers reject whenversion_request.chain_id != our_chain_id, checked before signature verification as a cheap network-membership gate (like the existing protocol-version check). Logsours/theirsfor operators.types.rs): a dedicatedRejectionReason::ChainIdMismatch, kept distinct fromProtocolMismatch(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.gossip_client.rs): when we are rejected withChainIdMismatchduring our own handshake, it maps to a terminalNetworkMismatchrejection 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 ourchain_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 seeChainIdMismatchare ones we're deliberately refusing because they're on a different chain.Test plan
rejection_reason_serializes_to_v1_wire_form,test_all_rejection_reasons_covered) — pass;fixtures/gossip_fixtures.jsonregeneratedpeer_discoveryintegration test extended with a validly-signed handshake declaring a foreignchain_id, assertingChainIdMismatch— pass (real V2 handler, end-to-end)cargo fmt,cargo clippyonirys-p2pandirys-chain-tests— clean🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests