Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/foreign-chain-config-tester/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ path = "src/main.rs"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true }
foreign-chain-health-check = { workspace = true, features = ["clap"] }
foreign-chain-health-check = { workspace = true }
mpc-node-config = { workspace = true }
serde = { workspace = true }
serde_yaml = { workspace = true }
Expand Down
28 changes: 8 additions & 20 deletions crates/foreign-chain-config-tester/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@ config, so a misconfiguration (unreachable URL, wrong/expired API key, or a
provider pointed at the wrong network) is caught before the node hits it in
production.

For each configured provider it runs a fixed request against a known reference
transaction — the same inspector and auth handling the node uses — and compares
the result against a known-good value. Sui, Starknet, Bitcoin, and the EVM chains
are the exceptions: they verify the provider's chain identity (a constant that is
never pruned) and then inspect a recently produced transaction — Sui from its
latest checkpoint, Starknet from its latest L1-accepted block (requires provider
JSON-RPC v0.9+), Bitcoin from a recent block (identity: the genesis block hash),
the EVM chains from the latest finalized block — so the check never depends on
months-old archived history. Every provider is checked independently: one bad
provider does not stop the others from being reported.
For each configured provider it verifies the provider's chain identity (a
constant that is never pruned) against the configured expected value, then runs
the node's real inspector — with the same auth handling the node uses — over a
recently produced transaction, so the check exercises the production path
without depending on months-old archived history. Every provider is checked
independently: one bad provider does not stop the others from being reported.

The expected identity of each identity-probed chain comes from configuration —
there are no built-in values, so the check works for any network, including
Expand All @@ -26,6 +22,7 @@ foreign_chain_health_check:
starknet: "0x534e5f4d41494e" # felt; decode hex as ASCII
base: "8453" # EVM numeric chain id
bitcoin: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" # genesis hash
aptos: "1" # ledger chain id
sui: "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S" # base58 genesis checkpoint digest
```

Expand All @@ -41,6 +38,7 @@ Well-known values:
| hyper_evm| `eth_chainId` | `999` | |
| abstract | `eth_chainId` | `2741` | `11124` |
| bitcoin | genesis block hash | `000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f` | `000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943` (testnet3) |
| aptos | ledger `chain_id` | `1` | `2` |
| sui | genesis digest (base58) | `4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S` | `69WiPg3DAQiwdxfncX6wYQ2siKwAe6L9BZthQea3JNMD` |

## Usage
Expand All @@ -56,16 +54,6 @@ cargo run -p foreign-chain-config-tester -- --config /path/to/user-config.toml
- the launcher config (`foreign_chains` under `node`);
- the legacy `config.yaml` (`foreign_chains` at the top level).

### Network

Reference transactions are network-specific. The network is auto-detected from
the config (`chain_id`, falling back to `mpc_contract_id`). Override it — or set
it for configs that carry no such field — with `--network`:

```bash
cargo run -p foreign-chain-config-tester -- --config user-config.toml --network testnet
```

## Output

A row per provider, a summary line, and the reason for each failure listed
Expand Down
110 changes: 2 additions & 108 deletions crates/foreign-chain-config-tester/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
use std::path::Path;

use anyhow::{Context, bail};
use mpc_node_config::{ChainId, ForeignChainsConfig};
use serde::Deserialize;
use serde::de::IntoDeserializer;
use serde::de::value::{Error as ValueError, StrDeserializer};
use mpc_node_config::ForeignChainsConfig;

use foreign_chain_health_check::{ExpectedIdentities, Network};
use foreign_chain_health_check::ExpectedIdentities;

/// Paths where `foreign_chains` may live, most-nested first so a wrapped config
/// matches before a barer one.
Expand All @@ -22,17 +19,6 @@ const FOREIGN_CHAINS_PATHS: &[&[&str]] = &[
&["foreign_chains"],
];

const CHAIN_ID_PATHS: &[&[&str]] = &[
&["mpc_node_config", "near_init", "chain_id"],
&["near_init", "chain_id"],
];

const CONTRACT_ID_PATHS: &[&[&str]] = &[
&["mpc_node_config", "node", "indexer", "mpc_contract_id"],
&["node", "indexer", "mpc_contract_id"],
&["indexer", "mpc_contract_id"],
];

/// Where the per-chain expected identities live: an `identities` map (chain label ->
/// expected identity) under a sibling of `foreign_chains`.
const EXPECTED_IDENTITY_PATHS: &[&[&str]] = &[
Expand All @@ -46,23 +32,6 @@ const EXPECTED_IDENTITY_PATHS: &[&[&str]] = &[
&["foreign_chain_health_check", "identities"],
];

fn classify_network(chain_id: Option<&str>, contract_id: Option<&str>) -> Option<Network> {
let parsed = chain_id.and_then(|id| {
let de: StrDeserializer<'_, ValueError> = id.into_deserializer();
ChainId::deserialize(de).ok()
});
match parsed {
Some(ChainId::Mainnet) => return Some(Network::Mainnet),
Some(ChainId::Testnet) => return Some(Network::Testnet),
_ => {}
}
match contract_id {
Some(id) if id.ends_with(".testnet") => Some(Network::Testnet),
Some(id) if id.ends_with(".near") || id == "v1.signer" => Some(Network::Mainnet),
_ => None,
}
}

enum Format {
Yaml,
Toml,
Expand Down Expand Up @@ -119,14 +88,6 @@ pub fn parse_foreign_chains(contents: &str, path: &Path) -> anyhow::Result<Forei
find_and_parse(contents, path, FOREIGN_CHAINS_PATHS, "foreign_chains")
}

fn toml_str<'a>(root: &'a toml::Value, path: &[&str]) -> Option<&'a str> {
path.iter().try_fold(root, |v, k| v.get(*k))?.as_str()
}

fn yaml_str<'a>(root: &'a serde_yaml::Value, path: &[&str]) -> Option<&'a str> {
path.iter().try_fold(root, |v, k| v.get(k))?.as_str()
}

/// 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(
Expand All @@ -141,27 +102,6 @@ pub fn detect_expected_identities(
)
}

/// `None` when the config carries no conclusive network signal.
pub fn detect_network(contents: &str, path: &Path) -> anyhow::Result<Option<Network>> {
Ok(match format_from_path(path)? {
Format::Yaml => {
let root: serde_yaml::Value =
serde_yaml::from_str(contents).context("parse YAML config")?;
classify_network(
CHAIN_ID_PATHS.iter().find_map(|p| yaml_str(&root, p)),
CONTRACT_ID_PATHS.iter().find_map(|p| yaml_str(&root, p)),
)
}
Format::Toml => {
let root: toml::Value = toml::from_str(contents).context("parse TOML config")?;
classify_network(
CHAIN_ID_PATHS.iter().find_map(|p| toml_str(&root, p)),
CONTRACT_ID_PATHS.iter().find_map(|p| toml_str(&root, p)),
)
}
})
}

#[cfg(test)]
#[expect(non_snake_case)]
mod tests {
Expand Down Expand Up @@ -262,52 +202,6 @@ foreign_chains:
assert!(error.contains("unsupported config extension"), "{error}");
}

#[test]
fn detect_network__should_read_chain_id_from_dstack_toml() {
// Given
let toml = "[mpc_node_config.near_init]\nchain_id = \"testnet\"\n";

// When
let network = detect_network(toml, Path::new("user-config.toml")).unwrap();

// Then
assert_eq!(network, Some(Network::Testnet));
}

#[test]
fn detect_network__should_fall_back_to_contract_id() {
// Given
let yaml = "indexer:\n mpc_contract_id: v1.signer-prod.testnet\n";

// When
let network = detect_network(yaml, Path::new("config.yaml")).unwrap();

// Then
assert_eq!(network, Some(Network::Testnet));
}

#[test]
fn detect_network__should_classify_mainnet_contract_id() {
// Given
let yaml = "indexer:\n mpc_contract_id: v1.signer\n";

// When
let network = detect_network(yaml, Path::new("config.yaml")).unwrap();

// Then
assert_eq!(network, Some(Network::Mainnet));
}

#[test]
fn detect_network__should_return_none_without_signal() {
// Given
// When
let network = detect_network("home_dir = \"/data\"\n", Path::new("config.toml")).unwrap();

// Then
assert_eq!(network, None);
}

#[test]
fn detect_expected_identities__should_read_seeded_value_from_dstack_toml() {
// Given an `identities` map nested under the dstack `mpc_node_config.node` prefix
Expand Down
27 changes: 6 additions & 21 deletions crates/foreign-chain-config-tester/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//! Foreign-chain RPC config tester: probe every configured provider with a fixed
//! golden request so operators can verify their config without running the node.
//! Sui, Starknet, Bitcoin, and the EVM chains are probed by chain identity plus
//! a dynamically discovered transaction instead — see the README.
//! Foreign-chain RPC config tester: probe every configured provider so operators can
//! verify their config without running the node. Each provider is checked by chain
//! identity plus a dynamically discovered transaction — see the README.

mod config;
mod report;
Expand All @@ -12,22 +11,17 @@ use std::process::ExitCode;

use anyhow::Context;
use clap::Parser;
use foreign_chain_health_check::{Network, check_all_providers};
use foreign_chain_health_check::check_all_providers;

/// Verify a node's foreign-chain RPC provider configuration.
///
/// Probes every configured provider against a known reference value.
/// Probes every configured provider by chain identity and a recent transaction.
#[derive(Parser)]
#[command(about, long_about = None)]
struct Args {
/// Path to the config file to check (`.yaml`, `.yml`, or `.toml`).
#[arg(long)]
config: PathBuf,

/// Network the reference values belong to. Auto-detected from
/// the config (`chain_id` / `mpc_contract_id`) when omitted.
#[arg(long, value_enum)]
network: Option<Network>,
}

#[tokio::main]
Expand All @@ -37,17 +31,8 @@ async fn main() -> anyhow::Result<ExitCode> {
.with_context(|| format!("failed to read {}", args.config.display()))?;
let foreign_chains = config::parse_foreign_chains(&contents, &args.config)?;
let identities = config::detect_expected_identities(&contents, &args.config)?;
let network = match args.network {
Some(network) => network,
None => config::detect_network(&contents, &args.config)?.ok_or_else(|| {
anyhow::anyhow!(
"could not determine network from config (no chain_id / mpc_contract_id found); \
pass --network mainnet|testnet"
)
})?,
};

let results = check_all_providers(&foreign_chains, network, &identities).await;
let results = check_all_providers(&foreign_chains, &identities).await;
print!("{}", report::render(&results));

Ok(if report::any_failed(&results) {
Expand Down
4 changes: 0 additions & 4 deletions crates/foreign-chain-health-check/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ version.workspace = true
edition.workspace = true
license.workspace = true

[features]
clap = ["dep:clap"]

[dependencies]
anyhow = { workspace = true }
bs58 = { workspace = true }
clap = { workspace = true, optional = true }
foreign-chain-inspector = { workspace = true }
foreign-chain-rpc-auth = { workspace = true }
foreign-chain-rpc-interfaces = { workspace = true }
Expand Down
Loading
Loading