Skip to content

Commit b060ade

Browse files
fix(node): say what the startup check skipped in operator terms
The warning named our own mechanism and left the operator to work out which of their chains went unchecked; it now lists them. The docs claimed a `ton` section exists, and that chains without an inspector report a status name an operator never sees.
1 parent 56ab66c commit b060ade

4 files changed

Lines changed: 17 additions & 19 deletions

File tree

crates/e2e-tests/tests/foreign_chain_probe.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ async fn foreign_chain_probe__should_publish_provider_health_on_startup() {
4242
setup_evm_mock(&server, MockAuthExpectation::None);
4343
let url = server.url("/");
4444

45-
let (cluster, _running) =
46-
common::must_setup_cluster(common::FOREIGN_CHAIN_PROBE_PORT_SEED, |c| {
45+
let (cluster, _running) = common::must_setup_cluster(
46+
common::FOREIGN_CHAIN_PROBE_PORT_SEED,
47+
|c: &mut e2e_tests::MpcClusterConfig| {
4748
c.num_nodes = 2;
4849
c.threshold = 2;
4950
c.foreign_chains.node_configs = vec![
@@ -56,8 +57,9 @@ async fn foreign_chain_probe__should_publish_provider_health_on_startup() {
5657
..Default::default()
5758
},
5859
];
59-
})
60-
.await;
60+
},
61+
)
62+
.await;
6163

6264
// when — the probe runs detached at startup, so both gauges settle on their own.
6365
common::wait_metric_on_nodes(

crates/node-config/src/foreign_chains.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -702,8 +702,6 @@ ckd:
702702
assert!(error.contains("support only header auth"), "{error}");
703703
}
704704

705-
/// `ForeignChain::label` promises the key an operator writes, and only this crate knows those
706-
/// keys.
707705
#[test]
708706
fn foreign_chains_config__should_key_every_chain_by_its_label() {
709707
// Given — every chain set, so a chain added later has to be listed here too.

crates/node/src/foreign_chain_probe.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ use tracing::{debug, info, warn};
1212

1313
use crate::metrics;
1414

15-
/// Asks every configured provider which network it serves and logs a line per provider plus an
16-
/// `x/y providers healthy` summary. Diagnostic only: a provider on the wrong network keeps
17-
/// serving, because a boot time blip should not take a chain out of signing.
15+
/// Asks every configured RPC provider which network it serves and logs a line per provider plus an
16+
/// `x/y providers healthy` summary. Diagnostic only.
1817
pub async fn run_startup_probe(foreign_chains: ForeignChainsConfig) {
1918
if foreign_chains.is_empty() {
2019
warn!("no foreign chain is configured: this node cannot verify foreign-chain transactions");
@@ -27,8 +26,6 @@ pub async fn run_startup_probe(foreign_chains: ForeignChainsConfig) {
2726
log_report(&report);
2827
}
2928

30-
/// Whether any probe covers this provider. What none covers stays out of both the summary and the
31-
/// gauges: it would otherwise read as an unhealthy provider for as long as the chain has no probe.
3229
fn is_probed(row: &ProviderHealth) -> bool {
3330
row.status != ProviderStatus::ProbeNotImplemented
3431
}
@@ -45,8 +42,6 @@ fn summarize(rows: &[ProviderHealth]) -> Summary {
4542
}
4643
}
4744

48-
/// A [`ProviderStatus`] carries no auth material and no rendered error text, and its one
49-
/// provider written field is length capped, so it is logged whole.
5045
fn log_report(report: &ProbeReport) {
5146
let rows = report.rows();
5247
for row in rows {
@@ -72,8 +67,10 @@ fn log_report(report: &ProbeReport) {
7267

7368
let Summary { probed, healthy } = summarize(rows);
7469
if probed == 0 {
70+
let chains: BTreeSet<&str> = rows.iter().map(|row| row.chain.label()).collect();
7571
warn!(
76-
"foreign-chain RPC provider probe found nothing to probe: no configured chain supports it"
72+
?chains,
73+
"no RPC provider was checked at startup: the foreign chains configured cannot be checked by the node"
7774
);
7875
return;
7976
}

docs/foreign-chain-transactions.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -544,13 +544,13 @@ Voting uses the protocol's existing signing threshold (`self.threshold()?.value(
544544

545545
The per-chain map key prevents *lookup* confusion: when the node resolves the operator's `ethereum:` section, only `entries[Ethereum]` is consulted, never `entries[Sepolia]`. What it doesn't prevent is a `ChainVote { chain: Ethereum, providers: [ProviderEntry { provider_id: "ankr", chain_routing: PathSegment { segment: "eth_sepolia" }, … }, …], threshold: _ }` getting voted in — the contract just stores what threshold consensus produces; it can't tell whether `"eth_sepolia"` actually corresponds to Ethereum mainnet. Threshold voter review is the first line of defense; the fan-out across a chain's providers is the structural one. The network fingerprint probe is a per-node diagnostic on top of both.
546546

547-
At startup, each configured provider gets its self-identifying RPC called and the response is compared against that chain's `expected_network_fingerprint` from the operator's config. The probe is report-only: a provider serving the wrong network is logged, but is not dropped, because a boot-time network blip should not take a chain out of signing. It runs detached, so it never delays startup.
547+
At startup, every provider of a chain the node can identify gets its self-identifying RPC called and the response is compared against that chain's `expected_network_fingerprint` from the operator's config. The probe is report-only: a provider serving the wrong network is logged, but is not dropped, because a boot-time network blip should not take a chain out of signing. It runs detached, so it never delays startup. A node that configures no foreign chain at all is warned about instead: the chains the code supports are the chains an operator is expected to configure.
548548

549549
The result is a line per provider, an `x/y providers healthy` summary counting only the providers a probe covers, and two gauges labelled by chain: `mpc_foreign_chain_rpc_providers_configured` and `mpc_foreign_chain_rpc_providers_healthy`. The gauges are per chain rather than per provider, because a provider name is operator chosen and would put an unbounded label on a time series. A chain whose providers cannot be identified, such as Solana or Ethereum, is left out of both the summary and the gauges: reporting `0` healthy against its configured count would read as every provider failing.
550550

551551
Taking the expected value from operator config rather than a constant in the attested binary is a deliberate trade. It makes mixed-network and local deployments checkable at all, since a config may pair one chain's mainnet with another's testnet and no binary can ship a value for a devnet. The cost is that the check no longer binds an operator: they can set the wrong value, or omit the field and get no check at all, and either way they fool only their own node's diagnostics. The network-level defenses against a wrong URL are unchanged: threshold voter review of the whitelist, and the provider fan-out, which fails the individual request when a provider disagrees with its siblings.
552552

553-
Every chain with an inspector is probed, each by the RPC below. `solana`, `ethereum` and `ton` have none, so they ignore `expected_network_fingerprint`. The fingerprint values themselves are tabulated once, under [Configuration (Node)](#configuration-node).
553+
Every chain with an inspector is probed, each by the RPC below. `solana` and `ethereum` have none, so they ignore `expected_network_fingerprint`. The fingerprint values themselves are tabulated once, under [Configuration (Node)](#configuration-node).
554554

555555
| chain | probe |
556556
|---|---|
@@ -720,9 +720,10 @@ each value must match the network of the `rpc_url` beside it. The value is alway
720720
including the fingerprints that look numeric.
721721

722722
Every chain with an inspector is probed, and for those, leaving the field unset is not a silent
723-
skip: every provider of the chain is reported as `MissingExpectedFingerprint`, because silence reads
724-
as healthy on a dashboard. `solana`, `ethereum` and `ton` have no inspector, so they report
725-
`ProbeNotImplemented` whether the field is set or not.
723+
skip: every provider of the chain is reported as `MissingExpectedFingerprint` and counts against the
724+
chain's healthy total, because silence reads as healthy on a dashboard. `solana` and `ethereum` have
725+
no inspector, so their providers are never asked and no health is reported for them, whether the
726+
field is set or not.
726727

727728
## Risks
728729

0 commit comments

Comments
 (0)