Skip to content

Commit 309cf51

Browse files
test(probe): tell the nodes apart by how many providers are healthy
Both nodes configure two BNB providers and expect the same network, so the healthy gauge reads 2 against 1. Neither is the gauge default, which a poll waiting for zero cannot tell from a probe that never ran. A new mock serves eth_chainId alone, all the probe asks, so the fuller EVM mock keeps its shape. Also covers the Fogo variant main added, converts the counts with try_from, and drops the label rstest the exhaustive node config test subsumes.
1 parent b61880b commit 309cf51

4 files changed

Lines changed: 76 additions & 55 deletions

File tree

crates/e2e-tests/src/foreign_chain_mock.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ impl MockServerExt {
7575
}
7676
}
7777

78-
pub const MOCK_EVM_CHAIN_ID: u64 = 8453;
7978
pub const MOCK_BLOCK_HASH: &str =
8079
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
8180
pub const MOCK_TX_ID: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
@@ -137,6 +136,28 @@ pub fn setup_bitcoin_mock(server: &MockServer, auth: MockAuthExpectation) -> usi
137136
mock_id
138137
}
139138

139+
pub fn setup_evm_chain_id_mock(server: &MockServer, chain_id: u64) -> usize {
140+
server
141+
.mock(|when, then| {
142+
when.method(POST);
143+
then.respond_with(move |req: &HttpMockRequest| {
144+
let body: serde_json::Value =
145+
serde_json::from_slice(req.body().as_ref()).expect("valid json-rpc request");
146+
let response_body = serde_json::json!({
147+
"jsonrpc": "2.0",
148+
"result": format!("{chain_id:#x}"),
149+
"id": body["id"].clone(),
150+
});
151+
HttpMockResponse::builder()
152+
.status(200)
153+
.header("content-type", "application/json")
154+
.body(serde_json::to_string(&response_body).unwrap())
155+
.build()
156+
});
157+
})
158+
.id
159+
}
160+
140161
pub fn setup_evm_mock(server: &MockServer, auth: MockAuthExpectation) -> usize {
141162
let mock_id = server.mock(|when, then| {
142163
auth.apply(when.method(POST));
@@ -147,7 +168,6 @@ pub fn setup_evm_mock(server: &MockServer, auth: MockAuthExpectation) -> usize {
147168
let method = body["method"].as_str().expect("method field");
148169

149170
let result = match method {
150-
"eth_chainId" => serde_json::json!(format!("{MOCK_EVM_CHAIN_ID:#x}")),
151171
"eth_getBlockByNumber" => {
152172
// First param is either a finality tag (e.g. "finalized") for the
153173
// finality-head lookup, or a `0x`-prefixed block number for the
Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,59 @@
1+
use std::collections::BTreeMap;
12
use std::num::NonZeroU64;
23

34
use crate::common;
45

5-
use e2e_tests::foreign_chain_mock::{MOCK_EVM_CHAIN_ID, MockAuthExpectation, setup_evm_mock};
6+
use e2e_tests::foreign_chain_mock::setup_evm_chain_id_mock;
67
use e2e_tests::{CLUSTER_WAIT_TIMEOUT, metrics};
78
use httpmock::MockServer;
89
use mpc_node_config::{
910
AuthConfig, ForeignChainConfig, ForeignChainProviderConfig, ForeignChainsConfig,
1011
};
11-
use near_mpc_bounded_collections::NonEmptyBTreeMap;
1212

13-
const HEALTHY_PROVIDER: &str = "healthy";
14-
const WRONG_NETWORK_PROVIDER: &str = "wrong-network";
15-
/// Any chain id the mock does not serve, so the probe reads a network mismatch.
13+
/// Belongs to no real chain: the probe only compares what a provider answers against config.
14+
const EXPECTED_NETWORK: u64 = 99999;
15+
/// Any chain id the expected mock does not serve, so the probe reads a network mismatch.
1616
const ANOTHER_NETWORK: u64 = 1;
1717

18-
fn base_chain_config(rpc_url: &str, expected_network: u64, provider: &str) -> ForeignChainConfig {
19-
ForeignChainConfig {
20-
timeout_sec: NonZeroU64::new(10).unwrap(),
21-
max_retries: NonZeroU64::new(1).unwrap(),
22-
expected_network_fingerprint: Some(expected_network.to_string()),
23-
providers: NonEmptyBTreeMap::new(
24-
provider.to_string().into(),
25-
ForeignChainProviderConfig {
26-
rpc_url: rpc_url.to_string(),
27-
auth: AuthConfig::None,
28-
},
29-
),
18+
fn bnb_chain_config(providers: &[(&str, &str)]) -> ForeignChainsConfig {
19+
let providers: BTreeMap<_, _> = providers
20+
.iter()
21+
.map(|(name, rpc_url)| {
22+
(
23+
name.to_string().into(),
24+
ForeignChainProviderConfig {
25+
rpc_url: rpc_url.to_string(),
26+
auth: AuthConfig::None,
27+
},
28+
)
29+
})
30+
.collect();
31+
ForeignChainsConfig {
32+
bnb: Some(ForeignChainConfig {
33+
timeout_sec: NonZeroU64::new(10).unwrap(),
34+
max_retries: NonZeroU64::new(1).unwrap(),
35+
expected_network_fingerprint: Some(EXPECTED_NETWORK.to_string()),
36+
providers: providers.try_into().expect("a named provider"),
37+
}),
38+
..Default::default()
3039
}
3140
}
3241

42+
/// Both nodes expect the same network and configure two BNB providers, so the healthy gauge
43+
/// separates them on one provider serving another network.
3344
#[tokio::test]
3445
#[expect(non_snake_case)]
3546
async fn foreign_chain_probe__should_publish_provider_health_on_startup() {
3647
// Given
37-
let server = MockServer::start();
38-
setup_evm_mock(&server, MockAuthExpectation::None);
39-
let url = server.url("/");
48+
let expected_network = MockServer::start();
49+
setup_evm_chain_id_mock(&expected_network, EXPECTED_NETWORK);
50+
let other_network = MockServer::start();
51+
setup_evm_chain_id_mock(&other_network, ANOTHER_NETWORK);
52+
53+
// The config rejects duplicate provider URLs, so the two sound providers take separate paths.
54+
let good_path = expected_network.url("/good_provider");
55+
let also_good_path = expected_network.url("/also_good_provider");
56+
let wrong_network = other_network.url("/wrong_network_provider");
4057

4158
// When
4259
let (cluster, _running) = common::must_setup_cluster(
@@ -45,18 +62,14 @@ async fn foreign_chain_probe__should_publish_provider_health_on_startup() {
4562
c.num_nodes = 2;
4663
c.threshold = 2;
4764
c.foreign_chains.node_configs = vec![
48-
ForeignChainsConfig {
49-
base: Some(base_chain_config(&url, MOCK_EVM_CHAIN_ID, HEALTHY_PROVIDER)),
50-
..Default::default()
51-
},
52-
ForeignChainsConfig {
53-
base: Some(base_chain_config(
54-
&url,
55-
ANOTHER_NETWORK,
56-
WRONG_NETWORK_PROVIDER,
57-
)),
58-
..Default::default()
59-
},
65+
bnb_chain_config(&[
66+
("good_provider", &good_path),
67+
("also_good_provider", &also_good_path),
68+
]),
69+
bnb_chain_config(&[
70+
("good_provider", &good_path),
71+
("wrong_network_provider", &wrong_network),
72+
]),
6073
];
6174
},
6275
)
@@ -67,29 +80,29 @@ async fn foreign_chain_probe__should_publish_provider_health_on_startup() {
6780
&cluster,
6881
&[0, 1],
6982
metrics::FOREIGN_CHAIN_RPC_PROVIDERS_CONFIGURED,
70-
|value| value == 1,
83+
|value| value == 2,
7184
CLUSTER_WAIT_TIMEOUT,
7285
)
7386
.await
74-
.expect("both nodes should report one configured Base provider");
87+
.expect("both nodes should report two configured BNB providers");
7588

7689
common::wait_metric_on_nodes(
7790
&cluster,
7891
&[0],
7992
metrics::FOREIGN_CHAIN_RPC_PROVIDERS_HEALTHY,
80-
|value| value == 1,
93+
|value| value == 2,
8194
CLUSTER_WAIT_TIMEOUT,
8295
)
8396
.await
84-
.expect("the provider serving the expected network should be healthy");
97+
.expect("both providers serving the expected network should be healthy");
8598

8699
common::wait_metric_on_nodes(
87100
&cluster,
88101
&[1],
89102
metrics::FOREIGN_CHAIN_RPC_PROVIDERS_HEALTHY,
90-
|value| value == 0,
103+
|value| value == 1,
91104
CLUSTER_WAIT_TIMEOUT,
92105
)
93106
.await
94-
.expect("the provider serving another network should not be healthy");
107+
.expect("only the provider serving the expected network should be healthy");
95108
}

crates/near-mpc-contract-interface/src/types/foreign_chain.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,7 @@ impl ForeignChain {
14081408
Self::Sui => "sui",
14091409
Self::Avalanche => "avalanche",
14101410
Self::Adi => "adi",
1411+
Self::Fogo => "fogo",
14111412
}
14121413
}
14131414
}
@@ -1976,19 +1977,6 @@ mod tests {
19761977
use super::*;
19771978
use rstest::rstest;
19781979

1979-
/// The two whose config key is not the variant name lowercased, which is where a label would
1980-
/// drift from what an operator writes.
1981-
#[rstest]
1982-
#[case::hyper_evm(ForeignChain::HyperEvm, "hyper_evm")]
1983-
#[case::abstract_chain(ForeignChain::Abstract, "abstract")]
1984-
fn label__should_name_the_chain_as_its_config_key(
1985-
#[case] chain: ForeignChain,
1986-
#[case] expected: &str,
1987-
) {
1988-
// When / Then
1989-
assert_eq!(chain.label(), expected);
1990-
}
1991-
19921980
#[test]
19931981
fn foreign_tx_sign_payload_v1_ethereum__should_have_consistent_hash() {
19941982
// Given

crates/node/src/foreign_chain_probe.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ fn publish_metrics(report: &ProbeReport) {
105105
}
106106
metrics::FOREIGN_CHAIN_RPC_PROVIDERS_CONFIGURED
107107
.with_label_values(&[chain.label()])
108-
.set(counts.configured as i64);
108+
.set(i64::try_from(counts.configured).expect("provider count never exceeds i64"));
109109
metrics::FOREIGN_CHAIN_RPC_PROVIDERS_HEALTHY
110110
.with_label_values(&[chain.label()])
111-
.set(counts.healthy as i64);
111+
.set(i64::try_from(counts.healthy).expect("provider count never exceeds i64"));
112112
}
113113
}
114114

0 commit comments

Comments
 (0)