Skip to content

feat: EVM chains RPC probing logic - #4049

Merged
haiyuechen-nearone merged 12 commits into
mainfrom
4003-probe-evm-chain-identity
Aug 7, 2026
Merged

feat: EVM chains RPC probing logic#4049
haiyuechen-nearone merged 12 commits into
mainfrom
4003-probe-evm-chain-identity

Conversation

@haiyuechen-nearone

@haiyuechen-nearone haiyuechen-nearone commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Closes #4096. Second slice of the identity probing series, after #4013

EvmInspector already serves all six chains, so eth_chainId covers Abstract, Arbitrum, Base, BNB, HyperEVM and Polygon at once.

Notes for review

  • Chain ids are compared in decimal, the form they are published and configured in, while eth_chainId answers a 0x hex quantity. Parsed as U256, since EIP-155 permits ids wider than a u64. Text that is neither reads back unchanged, so the report shows what the provider actually claimed.

  • The NetworkFingerprint length cap moved into NetworkFingerprint::new. Replaces the probe's bounded helper function. An answer past the cap ends in _TRUNCATED.

  • The injection of time and the RPC client (Inject the foreign chain probe's dependencies and report why setup failed #4043) is deliberately not folded in: I plan to refactor after all chains are wired up for startup probing.

Base automatically changed from 4003-read-foreign-chain-provider-identity to main August 4, 2026 21:15
@haiyuechen-nearone
haiyuechen-nearone force-pushed the 4003-probe-evm-chain-identity branch 2 times, most recently from baefb63 to c1c7b25 Compare August 5, 2026 13:06
@haiyuechen-nearone haiyuechen-nearone changed the title feat(probe): probe the EVM chains for their chain id feat: EVM chains RPC probing logic Aug 5, 2026
@haiyuechen-nearone
haiyuechen-nearone marked this pull request as ready for review August 5, 2026 15:02
@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

Pull request overview

Wires the startup network-fingerprint probe for the six EVM chains served by EvmInspector (Abstract, Arbitrum, Base, BNB, HyperEVM, Polygon) by implementing NetworkFingerprintInspector on EvmInspector over eth_chainId, and adds ChainIdResponse::canonical_text() to normalize the answer to decimal — the form EIP-155 ids are published and configured in. Parsing goes through U256, so ids wider than a u64 are handled, and non-numeric text reads back unchanged so the report shows what the provider actually claimed. The length cap on reported fingerprints moves out of the probe's bounded helper into NetworkFingerprint::new, where an over-long answer is cut short with a _TRUNCATED suffix.

Changes:

  • NetworkFingerprintInspector for EvmInspector (eth_chainId), plus probe_evm::<Chain> in the probe and one match arm per EVM chain; NO_PARAMS moved from the Starknet inspector to crate scope and shared.
  • New ChainIdResponse in the EVM RPC interfaces with hex/decimal canonicalization (0x/0X reads as hex, otherwise decimal, unparseable text returned verbatim) and an rstest table.
  • NetworkFingerprint::new takes over length capping (MAX_CHARS = 96, _TRUNCATED marker); the From derive and probe.rs::bounded are dropped.
  • Probe unit tests: an all-EVM-chains happy path, an EVM wrong-network case, and the not-implemented cases retargeted from Base to Bitcoin.
  • Six ignored live-RPC manual tests asserting each chain's fingerprint matches the shipped config value; RPC-URL consts hoisted to module scope.
  • docs/foreign-chain-transactions.md: probe table gains the eth_chainId row, normalization and truncation behavior documented.

Reviewed changes

Per-file summary
File Description
crates/foreign-chain-health-check/src/probe.rs Adds probe_evm + six match arms, removes bounded, narrows the TODO to Bitcoin/Aptos/Sui; new EVM tests and must_put_chain test helper
crates/foreign-chain-inspector/src/evm/inspector.rs Implements NetworkFingerprintInspector for EvmInspector via eth_chainId
crates/foreign-chain-inspector/src/lib.rs Adds NetworkFingerprint::new with the MAX_CHARS/_TRUNCATED cap and pub(crate) NO_PARAMS; drops the From derive; three cap unit tests
crates/foreign-chain-inspector/src/starknet/inspector.rs Uses the shared NO_PARAMS and routes construction through canonical_fingerprint
crates/foreign-chain-rpc-interfaces/src/evm.rs New ChainIdResponse + canonical_text() and its rstest table
crates/foreign-chain-inspector/tests/evm_inspector.rs Two network_fingerprint tests: decimal output, and that eth_chainId is the method called
crates/foreign-chain-inspector/tests/{abstract,arbitrum,base,bnb,hyperevm,polygon}_rpc_manual.rs Hoists the RPC URL const; adds an ignored live-provider fingerprint test per chain
docs/foreign-chain-transactions.md Probe table row for the EVM chains; normalization and truncation behavior described

Findings

No blocking issues. probe_all_providers still has no production caller (only check_all_providers is consumed, by foreign-chain-config-tester), so everything below is latent rather than operator-visible today.

Non-blocking (nits, follow-ups, suggestions):

  • crates/foreign-chain-rpc-interfaces/src/evm.rs:155#[case::padded_and_upper_cased("0x002105", "8453")] and its comment claim to cover an upper-cased answer, but 0x002105 has no alphabetic hex digits, so nothing is upper-cased. Compare crates/foreign-chain-rpc-interfaces/src/starknet.rs:187, where the same case name does exercise it (0x00534E5F4D41494E). The two probed chains whose ids contain letters are exactly the ones left untested — Abstract (0xab5) and Arbitrum (0xa4b1). Suggest adding #[case::upper_cased_digits("0xA4B1", "42161")].

  • crates/foreign-chain-inspector/src/lib.rs:60 — moving the cap into new() also moves it into the comparison path: probe_chain truncates both expected and observed before the equality check, whereas bounded() previously ran only on the way into WrongNetwork. Two distinct fingerprints sharing their first 86 characters now compare equal and report Healthy. No supported chain has a fingerprint anywhere near that long, so this is latent — but the const's own doc comment reserves headroom "for a chain that reports a longer one", which is precisely the case where it would bite. Either compare the untruncated canonical text and cap only for display, or state the invariant (MAX_CHARS must exceed every supported fingerprint) beside the const.

  • crates/foreign-chain-inspector/src/lib.rs:53 — "Bitcoin's genesis hash at 66 characters" is off by two: the shipped form is 64 hex characters with no 0x prefix (see the table at docs/foreign-chain-transactions.md:677, the config templates, and the test at crates/foreign-chain-inspector/src/lib.rs:719). Since that number is the whole justification for MAX_CHARS = 96, worth correcting.

  • crates/foreign-chain-rpc-interfaces/src/evm.rs:116canonical_text is applied to both the operator-written value and the provider's answer, so a bare decimal is accepted from the provider too, even though eth_chainId must answer a 0x quantity. A non-compliant provider that answers unprefixed hex is then read as decimal: one on chain 311 answering "137" reports Healthy against Polygon's "137". Contrived, and reading a bare decimal as decimal is the right call for a provider that conflates eth_chainId with net_version — but the leniency is only needed on the config side, so a strict 0x-only parse for responses would remove the false-healthy path entirely.

  • crates/foreign-chain-health-check/src/probe.rs:113 — with 7 of 13 ForeignChain variants now wired, the _ arm means a newly added chain silently lands in ProbeNotImplemented instead of failing to compile. Listing the remaining six explicitly would make the compiler force the decision the TODO(#4003) above describes.

  • crates/foreign-chain-health-check/src/probe.rs:111 — the TODO's reason for skipping Ethereum ("no inspector, so there is nothing to probe them with") is a type-level artifact rather than a real limitation: the NetworkFingerprintInspector impl at crates/foreign-chain-inspector/src/evm/inspector.rs:44 only needs Chain: Send + Sync, and ForeignChainsConfig has an ethereum slot operators can populate today. A marker type is all that stands between a configured ethereum section and an eth_chainId probe. (docs/foreign-chain-transactions.md:690 takes the same stance, so the PR is at least self-consistent — just worth revisiting under health-check: probe foreign chain RPC providers on node startup (chain identity only) #4003.)

  • crates/foreign-chain-health-check/src/probe.rs:835assert!(observed.to_string().chars().count() < 100) was written against the old ellipsis behavior; the contract is now exact (96 chars, _TRUNCATED suffix). The lib.rs unit tests pin that precisely, so this is only a missed opportunity to assert the marker here too.

✅ Approved

netrome
netrome previously approved these changes Aug 6, 2026

@netrome netrome left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A few nits, nothing blocking - though I did a relatively quick review

Comment on lines +241 to +250
/// Every EVM chain the probe covers: what `eth_chainId` answers on mainnet, and the decimal
/// chain id an operator configures.
const EVM_MAINNETS: [(ForeignChain, &str, &str); 6] = [
(ForeignChain::Abstract, "0xab5", "2741"),
(ForeignChain::Arbitrum, "0xa4b1", "42161"),
(ForeignChain::Base, "0x2105", "8453"),
(ForeignChain::Bnb, "0x38", "56"),
(ForeignChain::HyperEvm, "0x3e7", "999"),
(ForeignChain::Polygon, "0x89", "137"),
];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I find this hard to read. Could you add a struct instead of using nested tuples here? This would remove the need for the comment. I see how this is used in tests, but it would be nice with a struct like:

struct EvmMainnetFixture {
    chain: ForeignChain,
    expected: &'static str,
    answered: &'static str,
}

There are probably better field names here.

Also, the relation between "expected" and "answered" seems to be a hex to decimal conversion. Shouldn't this be handled by logic instead of duplicating the same information in the setup?

None => Self(within_cap),
Some(_) => {
let kept: String = within_cap.chars().take(KEPT_CHARS).collect();
Self(kept + Self::CUT_SHORT_MARKER)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Adding strings with + is a bit unconventional. I'd find using format! more readable here.

Suggested change
Self(kept + Self::CUT_SHORT_MARKER)
Self(format!{"{kept}{Self::CUT_SHORT_MARKER}"})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

anodar
anodar previously approved these changes Aug 7, 2026

@anodar anodar 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.

Thank you!

@haiyuechen-nearone
haiyuechen-nearone added this pull request to the merge queue Aug 7, 2026
@haiyuechen-nearone
haiyuechen-nearone removed this pull request from the merge queue due to a manual request Aug 7, 2026
@haiyuechen-nearone
haiyuechen-nearone dismissed stale reviews from anodar and netrome via a542873 August 7, 2026 10:11
@haiyuechen-nearone
haiyuechen-nearone force-pushed the 4003-probe-evm-chain-identity branch from 961f76d to 9058369 Compare August 7, 2026 10:31
One eth_chainId call covers Abstract, Arbitrum, Base, BNB, HyperEVM and
Polygon, since EvmInspector serves all of them. Chain ids are compared in
decimal, the form they are published and configured in, while the RPC
answers a hex quantity.
`NetworkFingerprint::new` crops, so the probe's `bounded` helper goes and no
inspector can forget the cap. Adds the live chain id check to each EVM manual
test, alongside Starknet's.
`MAX_CHARS` and the truncation marker are associated constants, so the test
reads the same values the cap is built from. Shares `NO_PARAMS` between the
inspectors.
`network_fingerprint` goes through `canonical_fingerprint`, the function the
operator's configured value already goes through. Documents what a report shows
for an answer that is no fingerprint or is too long, and keeps `U256` out of the
crate's public surface.
Also corrects the length of Bitcoin's genesis hash in the cap's doc, and states
that the cap must clear every fingerprint, since values are compared after the
cut.
@haiyuechen-nearone
haiyuechen-nearone force-pushed the 4003-probe-evm-chain-identity branch from 9058369 to 6585e55 Compare August 7, 2026 10:34

@netrome netrome left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for updating 🙏

Comment on lines +241 to +256
struct EvmMainnet {
chain: ForeignChain,
chain_id: u64,
}

impl EvmMainnet {
/// The form an operator configures.
fn expected(&self) -> String {
self.chain_id.to_string()
}

/// The `0xXXX` hex quantity an RPC provider answers to an `eth_chainId` request.
fn answered(&self) -> String {
format!("{:#x}", self.chain_id)
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice!

@haiyuechen-nearone
haiyuechen-nearone added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit eec052f Aug 7, 2026
15 checks passed
@haiyuechen-nearone
haiyuechen-nearone deleted the 4003-probe-evm-chain-identity branch August 7, 2026 12:12
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.

Probe the EVM chains for their chain id

3 participants