Skip to content

Commit 18aafe1

Browse files
pcarranzavclaude
andcommitted
feat: add RPC chain ID validation on oracle startup
Implements automatic validation of RPC endpoints to ensure they match the expected chain IDs from CAIP-2 identifiers. This prevents misconfiguration errors where an RPC URL points to the wrong chain. Features: - Validates protocol chain and all indexed chains on startup - Calls eth_chainId on each EVM RPC endpoint - Compares returned chain ID with expected value from CAIP-2 - Fails fast with clear error message on mismatch - Skips validation for non-EVM chains (non-eip155 namespace) Example output on success: ✓ Chain ID validated for eip155:42161: RPC https://arbitrum.example.com correctly returns chain ID 42161 Example output on failure: ERROR Chain ID mismatch for eip155:42161: RPC https://ethereum.example.com returned chain ID 1 (0x1), expected 42161 This addresses the safety requirement from TODO.md section 7. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9bd6517 commit 18aafe1

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use crate::config::{IndexedChain, ProtocolChain};
2+
use crate::runner::jrpc_utils::JrpcExpBackoff;
3+
use crate::{Caip2ChainId, Config};
4+
use anyhow::anyhow;
5+
use std::str::FromStr;
6+
use tracing::{error, info};
7+
use web3::api::Web3;
8+
use web3::helpers::CallFuture;
9+
10+
/// Validates that all configured RPC endpoints return the expected chain ID
11+
pub async fn validate_chain_ids(config: &Config) -> anyhow::Result<()> {
12+
info!("Validating RPC chain IDs...");
13+
14+
// Validate protocol chain
15+
validate_protocol_chain(&config.protocol_chain).await?;
16+
17+
// Validate indexed chains
18+
for chain in &config.indexed_chains {
19+
validate_indexed_chain(chain).await?;
20+
}
21+
22+
info!("All RPC chain IDs validated successfully");
23+
Ok(())
24+
}
25+
26+
async fn validate_protocol_chain(chain: &ProtocolChain) -> anyhow::Result<()> {
27+
let transport = JrpcExpBackoff::http(
28+
chain.jrpc_url.clone(),
29+
chain.id.clone(),
30+
std::time::Duration::from_secs(30),
31+
);
32+
let web3 = Web3::new(transport);
33+
34+
validate_chain_id(&web3, &chain.id, chain.jrpc_url.as_ref()).await
35+
}
36+
37+
async fn validate_indexed_chain(chain: &IndexedChain) -> anyhow::Result<()> {
38+
let transport = JrpcExpBackoff::http(
39+
chain.jrpc_url.clone(),
40+
chain.id.clone(),
41+
std::time::Duration::from_secs(30),
42+
);
43+
let web3 = Web3::new(transport);
44+
45+
validate_chain_id(&web3, &chain.id, chain.jrpc_url.as_ref()).await
46+
}
47+
48+
async fn validate_chain_id<T>(
49+
web3: &Web3<T>,
50+
expected_chain: &Caip2ChainId,
51+
rpc_url: &str,
52+
) -> anyhow::Result<()>
53+
where
54+
T: web3::Transport,
55+
{
56+
// Only validate EVM chains (namespace "eip155")
57+
if expected_chain.namespace_part() != "eip155" {
58+
info!(
59+
"Skipping chain ID validation for non-EVM chain: {}",
60+
expected_chain
61+
);
62+
return Ok(());
63+
}
64+
65+
// Parse expected numeric chain ID from CAIP-2 reference
66+
let expected_numeric_id = u64::from_str(expected_chain.reference_part())
67+
.map_err(|e| anyhow!("Failed to parse chain ID from {}: {}", expected_chain, e))?;
68+
69+
// Call eth_chainId
70+
let fut = web3.transport().execute("eth_chainId", vec![]);
71+
let call_fut: CallFuture<String, T::Out> = CallFuture::new(fut);
72+
73+
let chain_id_hex = match call_fut.await {
74+
Ok(id) => id,
75+
Err(e) => {
76+
error!("Failed to get chain ID from RPC {}: {}", rpc_url, e);
77+
return Err(anyhow!(
78+
"Failed to get chain ID from RPC {}: {}",
79+
rpc_url,
80+
e
81+
));
82+
}
83+
};
84+
85+
// Parse hex chain ID (e.g., "0xa4b1" -> 42161)
86+
let actual_chain_id = u64::from_str_radix(chain_id_hex.trim_start_matches("0x"), 16)
87+
.map_err(|e| anyhow!("Failed to parse chain ID hex '{}': {}", chain_id_hex, e))?;
88+
89+
// Compare
90+
if actual_chain_id != expected_numeric_id {
91+
error!(
92+
"Chain ID mismatch for {}: RPC {} returned chain ID {} (0x{:x}), expected {} from CAIP-2 identifier {}",
93+
expected_chain, rpc_url, actual_chain_id, actual_chain_id, expected_numeric_id, expected_chain
94+
);
95+
return Err(anyhow!(
96+
"Chain ID mismatch for {}: RPC {} returned chain ID {} (0x{:x}), expected {} from CAIP-2 identifier {}",
97+
expected_chain, rpc_url, actual_chain_id, actual_chain_id, expected_numeric_id, expected_chain
98+
));
99+
}
100+
101+
info!(
102+
"✓ Chain ID validated for {}: RPC {} correctly returns chain ID {}",
103+
expected_chain, rpc_url, actual_chain_id
104+
);
105+
106+
Ok(())
107+
}
108+
109+
#[cfg(test)]
110+
mod tests {
111+
use super::*;
112+
113+
#[test]
114+
fn test_caip2_parsing() {
115+
let chain_id = Caip2ChainId::from_str("eip155:1").unwrap();
116+
assert_eq!(chain_id.namespace_part(), "eip155");
117+
assert_eq!(chain_id.reference_part(), "1");
118+
119+
let chain_id = Caip2ChainId::from_str("eip155:42161").unwrap();
120+
assert_eq!(chain_id.namespace_part(), "eip155");
121+
assert_eq!(chain_id.reference_part(), "42161");
122+
123+
// Non-EVM chain
124+
let chain_id = Caip2ChainId::from_str("bip122:000000000019d6689c085ae165831e93").unwrap();
125+
assert_eq!(chain_id.namespace_part(), "bip122");
126+
assert_eq!(
127+
chain_id.reference_part(),
128+
"000000000019d6689c085ae165831e93"
129+
);
130+
}
131+
}

crates/oracle/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod chain_validation;
12
pub mod commands;
23
pub mod config;
34
pub mod contracts;

crates/oracle/src/runner/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ pub async fn run(config_file: impl AsRef<Path>) -> Result<(), Error> {
7171
init_logging(config.log_level);
7272
info!(log_level = %config.log_level, "The block oracle is starting.");
7373

74+
// Validate RPC chain IDs before starting
75+
if let Err(err) = crate::chain_validation::validate_chain_ids(&config).await {
76+
error!("Chain ID validation failed: {}", err);
77+
return Err(Error::BadJrpcProtocolChain(web3::Error::Decoder(
78+
err.to_string(),
79+
)));
80+
}
81+
7482
// Spawn the metrics server
7583
tokio::spawn(metrics_server(&METRICS, config.metrics_port));
7684

0 commit comments

Comments
 (0)