Skip to content

feat: probe Starknet by chain id and a recent transaction - #3932

Open
haiyuechen-nearone wants to merge 1 commit into
mainfrom
3764-foreign-chain-health-hybrid-starknet
Open

feat: probe Starknet by chain id and a recent transaction#3932
haiyuechen-nearone wants to merge 1 commit into
mainfrom
3764-foreign-chain-health-hybrid-starknet

Conversation

@haiyuechen-nearone

@haiyuechen-nearone haiyuechen-nearone commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Part of #3764; rationale: #3969. Stacked, merge bottom-up:

  1. feat: probe Starknet by chain id and a recent transaction #3932 (this PR) — Starknet: chain id + a recent L1-accepted tx
  2. feat(health-check): probe EVM chains by chain id and a recent finalized transaction #3962 — 6 EVM chains: eth_chainId + a recent finalized tx
  3. feat(health-check): probe Bitcoin by genesis hash and a recent transaction #3963 — Bitcoin: genesis hash + a recent tx
  4. feat(health-check): probe Aptos by ledger chain id and a recent transaction #3964 — Aptos: ledger chain id + a recent tx

Replaces Starknet's pinned golden tx with the Sui-style probe: verify starknet_chainId, then inspect a real tx from a recent l1_accepted block at AcceptedOnL1 (walks back past empty/not-yet-final blocks; needs provider JSON-RPC v0.9+).

Identities come from config (foreign_chain_health_check.identities), no built-ins — so any network is checkable, and a configured chain fails until its identity is set. Also lands the shared scaffolding the rest of the stack reuses.

@haiyuechen-nearone
haiyuechen-nearone force-pushed the 3764-foreign-chain-health-hybrid-starknet branch from 740fa83 to 40098ed Compare July 24, 2026 12:32
@haiyuechen-nearone haiyuechen-nearone changed the title feat(health-check): Starknet chain-id + dynamic-tx probe (Option B prototype) feat(health-check): probe Starknet by chain id and a recent L1-accepted transaction Jul 24, 2026
@haiyuechen-nearone
haiyuechen-nearone force-pushed the 3764-foreign-chain-health-hybrid-starknet branch 2 times, most recently from a79a492 to dc7ac79 Compare July 27, 2026 10:31
@haiyuechen-nearone haiyuechen-nearone changed the title feat(health-check): probe Starknet by chain id and a recent L1-accepted transaction feat(health-check): probe Starknet by chain id and a recent transaction Jul 27, 2026
@haiyuechen-nearone
haiyuechen-nearone force-pushed the 3764-foreign-chain-health-hybrid-starknet branch 14 times, most recently from b2f1b90 to 0d17ad2 Compare July 27, 2026 12:42
Replace Starknet's pinned golden transaction with the Sui-style hybrid probe:
verify the chain identity (starknet_chainId, a genesis-derived constant that is
never pruned), then run the real inspector over a transaction from the latest
L1-accepted block at AcceptedOnL1 finality. This removes the archive-node
dependency and the per-network golden-vector maintenance while keeping the
production-path smoke test (receipt -> finality -> canonicality).

AcceptedOnL1 matches what real verify-foreign-tx requests use and actually
exercises the finality check (the AcceptedOnL2 branch is a no-op). Discovery
uses the l1_accepted block tag, so the probe requires provider JSON-RPC v0.9+.

To stay robust against load-balanced providers whose backends disagree on the
L1-accepted frontier, the probe walks back from the head — doubling the step
each time, capped at MAX_WALKBACK_BLOCKS — past empty blocks and past receipts
a lagging backend still reports below L1 finality.

Expected identities carry no built-in values: they come from configuration
(foreign_chain_health_check.identities), so any network — including local or
custom ones — is checkable, and a configured identity-probed chain fails until
its identity is set. Sui's built-in reference moves to configuration on the
same terms. Operator templates ship with the identities prefilled.
@haiyuechen-nearone
haiyuechen-nearone force-pushed the 3764-foreign-chain-health-hybrid-starknet branch from 0d17ad2 to c38ac5d Compare July 27, 2026 12:50
@haiyuechen-nearone
haiyuechen-nearone marked this pull request as ready for review July 27, 2026 12:54
@haiyuechen-nearone haiyuechen-nearone changed the title feat(health-check): probe Starknet by chain id and a recent transaction feat: probe Starknet by chain id and a recent transaction Jul 27, 2026
@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Pull request overview

Replaces Starknet's pinned golden transaction with a Sui-style identity + recent-transaction probe: verifies starknet_chainId matches configuration, then walks back from the L1-accepted head to find a real transaction the inspector can verify at AcceptedOnL1. Removes built-in identity constants for both Starknet and Sui — they now come from foreign_chain_health_check.identities in the node config — and extends StarknetInspector/BlockId with the required chain_id and L1Accepted-tag support. Bundled scaffolding (config discovery, ExpectedIdentities, mark_missing_identity) is shared with the follow-up chains in the stack.

Changes:

  • check_starknet: new signature (client, expected_chain_id); probes chain id, then walks back exponentially from head − 10 past empty/NotFinalized blocks (cap MAX_WALKBACK_BLOCKS = 1024) — TransactionFailed / LogIndexOutOfBounds count as success (canonical data was served).
  • StarknetInspector gains chain_id() and public block_with_tx_hashes(); canonical-block helper refactored to reuse the new getter.
  • BlockId gains a Tag(BlockTag) variant serialized as a bare JSON-RPC string; adds BlockTag::L1Accepted (requires provider JSON-RPC v0.9+) and ChainIdArgs; parse_felt becomes pub.
  • GetBlockWithTxHashesResponse.transactions is now populated (with #[serde(default)] fallback for older responses).
  • golden.rs drops SuiVector and Starknet from GoldenSet; docstring updated.
  • New ExpectedIdentities struct (deny_unknown_fields) in the health-check crate; new mark_missing_identity yields per-provider Failed rows when a probed chain has no identity configured.
  • foreign-chain-config-tester: extracts a generic find_and_parse<T> helper and adds detect_expected_identities; wires it into main.
  • Deployment config (cvm-deployment/user-config.toml) adds a Sui provider block plus the new identities map; localnet templates seed the mainnet identities.
  • New SequentialMockClient powers four scripted check_starknet tests (pass, chain-id mismatch, empty-block walk-back, AcceptedOnL2 walk-back, cap-exceeded).

Reviewed changes

Per-file summary
File Description
crates/foreign-chain-health-check/src/checks.rs Rewrites check_starknet as an identity + exponential walk-back probe over L1Accepted; adds SequentialMockClient and four new tests; updates Sui tests to hard-coded chain-id constants.
crates/foreign-chain-health-check/src/lib.rs Introduces ExpectedIdentities; threads &ExpectedIdentities through check_all_providers; run_starknet/run_sui take Option<&str> and use mark_missing_identity; drops the now-unused Sui/Starknet golden imports.
crates/foreign-chain-health-check/src/golden.rs Removes SuiVector and the Starknet entries from GoldenSet; docstring and mainnet/testnet vectors updated.
crates/foreign-chain-health-check/Cargo.toml Adds jsonrpsee + serde to dependencies (used in the new inspector call signatures / tests); adds serde again to dev-dependencies (redundant — see nit).
crates/foreign-chain-inspector/src/starknet/inspector.rs Adds chain_id() and public block_with_tx_hashes(); canonical check reuses the getter.
crates/foreign-chain-inspector/tests/starknet_inspector.rs Adds the new transactions: vec![] field to two fixtures.
crates/foreign-chain-rpc-interfaces/src/starknet.rs Adds BlockTag::L1Accepted, ChainIdArgs, and GetBlockWithTxHashesResponse.transactions; makes parse_felt pub; new serialization test asserting the tag renders as "l1_accepted".
crates/foreign-chain-config-tester/src/config.rs Extracts generic find_and_parse<T>; adds detect_expected_identities with five tests (dstack path, YAML top-level, non-string value, unknown key, absent section).
crates/foreign-chain-config-tester/src/main.rs Loads identities from the same config and passes them to check_all_providers; doc comments updated.
crates/foreign-chain-config-tester/README.md Documents identity-probe chains, config schema, and the well-known values table.
deployment/cvm-deployment/user-config.toml, docs/localnet/mpc-config.template.toml, docs/localnet/mpc-configs/config.yaml.template Adds the new foreign_chain_health_check.identities section; deployment additionally seeds a Sui provider.

Findings

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

  • crates/foreign-chain-health-check/src/checks.rs:203 — Message says "no L1-final transaction within MAX_WALKBACK_BLOCKS blocks of the probe block", but the exponential walk-back only samples ~11 blocks in that window (1, 2, 4, …, 512, total 1023 blocks walked back), so the message overstates how thoroughly the range was probed. Consider rewording to "in {n} samples across {walked_back} blocks" or similar, so an operator debugging a real outage does not chase a wrong-provider hypothesis.
  • crates/foreign-chain-health-check/src/checks.rs:149-155Mismatch::ChainId prints expected as the raw configured string (e.g. 0x534e5f4d41494e) while got is the fully-padded 32-byte hex (0x0000…534e5f4d41494e). The two look confusingly different at a glance. Suggest normalizing both through format!(\"{:#x}\", …) on an H256 (or trimming leading zeros in got) so operators can eyeball equality.
  • crates/foreign-chain-health-check/src/checks.rs:182 — Only the block's first transaction is inspected. That's usually fine (TransactionFailed/LogIndexOutOfBounds already count as success), but if the first tx happens to hit NonCanonicalBlock or MalformedRpcResponse while other txs in the same block are healthy, the probe bails on a real error before trying them. Worth a follow-up if you see false negatives from lagging load-balancers on the receipt path; not a hard blocker because the outer inspector semantics for those errors are "real problem, surface it."
  • crates/foreign-chain-health-check/Cargo.toml:20-27serde = { workspace = true } is listed in both [dependencies] and [dev-dependencies]. The dev entry is redundant — dependencies already covers test code.
  • crates/foreign-chain-health-check/src/checks.rs:502-545 — In check_starknet__should_walk_back_when_the_probe_tx_is_not_yet_l1_final, the earlier block's tx ([4;32]) is set up at block 899_989 but the receipt returned for it reports block_number: 842_750 (from starknet_receipt()). That's internally inconsistent even though the sequential mock doesn't care about it — real Starknet receipts point back to the containing block. Consider tweaking the fixtures so the receipt block number matches, otherwise the test reads as if the canonical-block check would fail in reality.
  • crates/foreign-chain-inspector/src/starknet/inspector.rs:92-93 — Doc comment names Latest as an example ("e.g. BlockId::Tag(Latest)"), but BlockTag only exposes L1Accepted in this PR. Either mention L1Accepted in the example or leave the example open-ended.
  • deployment/cvm-deployment/user-config.toml:151-163 — This PR is titled "Starknet" but the same file gets a whole new Sui provider block. Not wrong (Sui also transitions to identity-based here), but flagging in case it wasn't intentional in the stack ordering.

✅ Approved

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

So we need to discuss this further. Now this PR modifies a separate health check CLI. For this CLI, I'm not as convinced that querying transactions is wrong. I can see the point of doing that as long as it's stand-alone. Additionally, I am not too fond of the idea of reusing too much of the foreign-chain-health-check crate in our node code as-is, as it seems to me like a vibe-coded CLI with questionable code-quality that only passed shallow reviews in: #3716

There's a lot of code smell (large functions, helpers mutating shared collections instead of being pure functions that could be executed in parallel and tested more cleanly) that I'd be very hesitant to bring into node code.

Finally, I don't think it should be too complicated to just extend our inspector without touching any of the foreign-chain-config-tester code. I'd picture us adding some kind of trait to get the chain identity of any inspector and checking these against configured values on startup. I think this could greatly simplify things.

I'll schedule a call so we can chat about this tomorrow.


/// Per-chain expected identities from config. Absent chains stay `None` (their check then
/// fails until configured); an unknown chain key or non-string value is a hard error.
pub fn detect_expected_identities(

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.

Nit: What do you think about renaming this to parse_expected_identities to maintain parity with parse_foreign_chains?

Comment on lines +332 to +338
fn mark_missing_identity(
chain: &'static str,
cfg: &ForeignChainConfig,
out: &mut Vec<ProviderResult>,
) {
for name in cfg.providers.keys() {
out.push(ProviderResult {

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.

Hmm not a fan of pushing to a mutable vector here. I see this was the established pattern, but we should consider refactoring this for a cleaner and more maintainable implementation now when this becomes part of our node code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Something like this maybe?

enum HealthCheckStatus {
    Healthy
    Unhealthy
}

struct ForeignChainProviderState {
    provider_name: String
    health_check_status: HealthCheckStatus
}

struct ForeignChainSnapshot {
    chain: ForeignChain
    configured: bool
    providers_state: Vec<ForeignChainProviderState>
}

struct ForeignChainProvidersSnapshot {
    snapshots: Vec<ForeignChainSnapshot>
}

My habit is to introduce minimal changes for each PR, and leave any refactors for a separate PR where there is no behavior change. I can try to put the refactors upfront if that is the preferred work flow.

Comment on lines +159 to +163
# Expected identity per chain for the provider health check — see
# crates/foreign-chain-config-tester/README.md for the well-known values.
[mpc_node_config.node.foreign_chain_health_check.identities]
starknet = "0x534e5f5345504f4c4941" # mainnet: "0x534e5f4d41494e"
sui = "69WiPg3DAQiwdxfncX6wYQ2siKwAe6L9BZthQea3JNMD" # mainnet: "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S"

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.

This looks really nice.

@haiyuechen-nearone

Copy link
Copy Markdown
Contributor Author

Now this PR modifies a separate health check CLI.

My intention was that the CLI and the healthcheck should share the core probing logic, so that the same code flow can be tested against real providers easily through the CLI (which is what I have tested this PR with as well). And then by extension the CLI needed the changes because of the config changes.

@netrome

netrome commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

My intention was that the CLI and the healthcheck should share the core probing logic, so that the same code flow can be tested against real providers easily through the CLI (which is what I have tested this PR with as well). And then by extension the CLI needed the changes because of the config changes.

Fair point, I'm happy to share the probing logic between the two. However, the shared surface should uphold production standards that I'm not convinced that the CLI does. To achieve this shared surface, I believe the simplest approach would be to introduce the chain ID probes in the node first, and then reuse those probes in the CLI.

I'm happy to do it the other way around, but this front-loads much more code cleanups in the health-check and it's harder to picture how the probes will be integrated in the node requiring more context in reviews.

@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

PR title type suggestion: This PR changes only configuration and asset files (VS Code settings, fonts, .gitignore), not source code. The type prefix should probably be chore: instead of feat:.

Suggested title: chore: update VS Code config and font assets

(If this PR actually does modify source code for Starknet probing, please verify the changed files list is complete.)

@andrei-near
andrei-near force-pushed the 3764-foreign-chain-health-hybrid-starknet branch from 93c9ab5 to c38ac5d Compare August 1, 2026 16:02
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.

2 participants