diff --git a/crates/foreign-chain-config-tester/README.md b/crates/foreign-chain-config-tester/README.md index d7298562cc..f0e88cce9b 100644 --- a/crates/foreign-chain-config-tester/README.md +++ b/crates/foreign-chain-config-tester/README.md @@ -7,13 +7,13 @@ 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 and Starknet are the exceptions: they -verify the provider's chain identity (a genesis-derived constant that is never +the result against a known-good value. Sui, Starknet, 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+) — 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. +v0.9+), 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. 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 @@ -23,6 +23,7 @@ local or custom ones. A configured chain without an identity fails its check: foreign_chain_health_check: identities: starknet: "0x534e5f4d41494e" # felt; decode hex as ASCII + base: "8453" # EVM numeric chain id sui: "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S" # base58 genesis checkpoint digest ``` @@ -31,6 +32,12 @@ Well-known values: | Chain | Identity | Mainnet | Testnet | |----------|-------------------------|------------------------------------------------|------------------------------------------------| | starknet | `starknet_chainId` felt | `0x534e5f4d41494e` (`SN_MAIN`) | `0x534e5f5345504f4c4941` (`SN_SEPOLIA`) | +| base | `eth_chainId` | `8453` | | +| bnb | `eth_chainId` | `56` | | +| arbitrum | `eth_chainId` | `42161` | | +| polygon | `eth_chainId` | `137` | | +| hyper_evm| `eth_chainId` | `999` | | +| abstract | `eth_chainId` | `2741` | `11124` | | sui | genesis digest (base58) | `4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S` | `69WiPg3DAQiwdxfncX6wYQ2siKwAe6L9BZthQea3JNMD` | ## Usage diff --git a/crates/foreign-chain-config-tester/src/main.rs b/crates/foreign-chain-config-tester/src/main.rs index ef132e278c..b0fa9d71c4 100644 --- a/crates/foreign-chain-config-tester/src/main.rs +++ b/crates/foreign-chain-config-tester/src/main.rs @@ -1,7 +1,7 @@ //! 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 and Starknet are probed by chain identity plus a dynamically discovered -//! transaction instead — see the README. +//! Sui, Starknet, and the EVM chains are probed by chain identity plus a +//! dynamically discovered transaction instead — see the README. mod config; mod report; diff --git a/crates/foreign-chain-health-check/src/checks.rs b/crates/foreign-chain-health-check/src/checks.rs index 492f412cd2..a3732d1d18 100644 --- a/crates/foreign-chain-health-check/src/checks.rs +++ b/crates/foreign-chain-health-check/src/checks.rs @@ -1,6 +1,6 @@ //! Per-provider checks. Golden-transaction chains run a fixed request and verify the -//! extracted value; identity-based chains (Sui, Starknet) verify the chain identity and -//! inspect a dynamically discovered recent transaction instead. +//! extracted value; identity-based chains (Sui, Starknet, the EVM chains) verify the chain +//! identity and inspect a dynamically discovered recent transaction instead. use std::time::Duration; @@ -16,7 +16,7 @@ use foreign_chain_inspector::{ BitcoinExtractedValue, BitcoinTransactionHash, inspector::{BitcoinExtractor, BitcoinInspector}, }, - evm::inspector::{EvmChain, EvmExtractedValue, EvmExtractor, EvmInspector}, + evm::inspector::{EvmChain, EvmExtractor, EvmInspector}, http_client::HttpClient, starknet::{ StarknetTransactionHash, @@ -28,6 +28,7 @@ use foreign_chain_inspector::{ }, }; use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient; +use foreign_chain_rpc_interfaces::evm::{BlockNumberOrTag, FinalityTag, U64}; use foreign_chain_rpc_interfaces::starknet::{BlockId, BlockTag}; use foreign_chain_rpc_interfaces::sui::SuiRpcClient; use http::{HeaderName, HeaderValue}; @@ -80,29 +81,102 @@ fn verify_block_hash(expected: [u8; 32], got: [u8; 32]) -> anyhow::Result<()> { Ok(()) } -pub async fn check_evm( - client: HttpClient, - tx: [u8; 32], - expected_block_hash: [u8; 32], -) -> anyhow::Result<()> +/// How far below a chain's reported head a probe takes its block, so slightly lagging backends +/// behind one provider URL still agree the block is final. +const HEAD_PROBE_OFFSET: u64 = 10; + +/// How many earlier blocks a probe scans when a block carries no transactions (quiet networks +/// still produce blocks on a timer, so empty blocks are normal). +const EMPTY_BLOCK_WALKBACK_LIMIT: u64 = 10; + +/// Treats a probe transaction as healthy when it verifies, or when it fails in a way that +/// reflects the transaction itself (reverted, or no log at the index) rather than the provider — +/// both still prove the provider serves canonical, final data. Any other error is a real failure. +fn accept_probe_outcome( + outcome: Result, ForeignChainInspectionError>, + failure_context: &'static str, +) -> anyhow::Result<()> { + match outcome { + Ok(_) + | Err(ForeignChainInspectionError::TransactionFailed) + | Err(ForeignChainInspectionError::LogIndexOutOfBounds) => Ok(()), + Err(e) => Err(e).context(failure_context), + } +} + +/// Verifies the network via `eth_chainId`, then runs the inspector at `Finalized` over a +/// transaction from a recent finalized block — walking back past empty ones. +pub async fn check_evm(client: C, expected_chain_id: &str) -> anyhow::Result<()> where Chain: EvmChain + Send + Sync, + C: ClientT + Send + Sync, { - let inspector = EvmInspector::::new(client); - let values = inspector + let inspector = EvmInspector::::new(client); + + let expected = golden::chain_id_u64(expected_chain_id).context("invalid expected chain id")?; + let got = inspector + .chain_id() + .await + .context("failed to fetch chain id")?; + if got != expected { + return Err(Mismatch::ChainId { + expected: expected.to_string(), + got: got.to_string(), + } + .into()); + } + + let head = inspector + .block_with_txs(BlockNumberOrTag::Tag(FinalityTag::Finalized)) + .await + .context("failed to fetch the finalized block")?; + let probe_number = head + .number + .as_u64() + .checked_sub(HEAD_PROBE_OFFSET) + .with_context(|| { + format!( + "finalized height {} is below the probe offset {HEAD_PROBE_OFFSET}", + head.number + ) + })?; + let mut block = inspector + .block_with_txs(BlockNumberOrTag::Number(U64::from(probe_number))) + .await + .context("failed to fetch the probe block")?; + let mut walked_back = 0; + let tx = loop { + if let Some(tx) = block.transactions.first() { + break Chain::TransactionHash::from(*tx.as_fixed_bytes()); + } + walked_back += 1; + if walked_back > EMPTY_BLOCK_WALKBACK_LIMIT { + bail!( + "no transactions in the probe block or the {EMPTY_BLOCK_WALKBACK_LIMIT} blocks before it" + ); + } + let earlier = block + .number + .as_u64() + .checked_sub(1) + .context("walked back past the genesis block without finding a transaction")?; + block = inspector + .block_with_txs(BlockNumberOrTag::Number(U64::from(earlier))) + .await + .context("failed to fetch an earlier finalized block")?; + }; + + let outcome = inspector .extract( - Chain::TransactionHash::from(tx), + tx, EthereumFinality::Finalized, - vec![EvmExtractor::BlockHash], + vec![EvmExtractor::Log { log_index: 0 }], ) - .await?; - match values.into_iter().next().context("RPC returned no value")? { - EvmExtractedValue::BlockHash(hash) => { - let got: [u8; 32] = hash.into(); - verify_block_hash(expected_block_hash, got) - } - EvmExtractedValue::Log(_) => bail!("expected a block hash, got a log"), - } + .await; + accept_probe_outcome( + outcome, + "failed to inspect a transaction from the finalized block", + ) } pub async fn check_bitcoin( @@ -126,9 +200,6 @@ pub async fn check_bitcoin( } } -/// Where probing starts: this many blocks below the L1-accepted head. -const HEAD_PROBE_OFFSET: u64 = 10; - /// Cap on the exponential walk-back (the step doubles each try) before giving up. const MAX_WALKBACK_BLOCKS: u64 = 1024; @@ -263,22 +334,17 @@ pub async fn check_sui(client: impl SuiRpcClient, expected_chain_id: &str) -> an let tx = golden::base58_32(digest)?; let inspector = SuiInspector::new(client); - // Probe the first event to exercise the full extraction pipeline when the tx emits - // events; a tx with no events (`LogIndexOutOfBounds`) or a failed one still proves the - // provider serves canonical checkpointed data. - match inspector + let outcome = inspector .extract( SuiTransactionDigest::from(tx), SuiFinality::Checkpointed, vec![SuiExtractor::Event { event_index: 0 }], ) - .await - { - Ok(_) - | Err(ForeignChainInspectionError::TransactionFailed) - | Err(ForeignChainInspectionError::LogIndexOutOfBounds) => Ok(()), - Err(e) => Err(e).context("failed to inspect a transaction from the latest checkpoint"), - } + .await; + accept_probe_outcome( + outcome, + "failed to inspect a transaction from the latest checkpoint", + ) } pub async fn check_aptos( @@ -325,6 +391,11 @@ mod tests { use crate::golden; use crate::network::Network; use assert_matches::assert_matches; + use foreign_chain_inspector::base::inspector::Base; + use foreign_chain_rpc_interfaces::evm::{ + GetBlockByNumberResponse as EvmBlock, GetBlockByNumberWithTxsResponse as EvmBlockWithTxs, + GetTransactionReceiptResponse as EvmReceipt, U64, + }; use foreign_chain_rpc_interfaces::starknet::{ GetBlockWithTxHashesResponse, GetTransactionReceiptResponse, H256, StarknetEvent, StarknetExecutionStatus, StarknetFinalityStatus, @@ -572,6 +643,153 @@ mod tests { assert!(error.contains("no L1-final transaction"), "{error}"); } + const BASE_MAINNET_CHAIN_ID: &str = "8453"; + + #[tokio::test] + async fn check_evm__should_pass_when_chain_id_matches_and_a_recent_tx_verifies() { + // Given a provider on the expected network whose probe block (10 below the finalized + // head) carries a transaction the inspector can verify (finalized, canonical, succeeded; + // no logs -> the accept list treats an out-of-bounds log index as healthy). + let tx = H256::from([3; 32]); + let block_hash = H256::from([11; 32]); + let receipt = EvmReceipt { + transaction_hash: tx, + block_hash, + block_number: U64::from(50), + status: U64::from(1), + logs: vec![], + }; + let head = EvmBlockWithTxs { + number: U64::from(100), + hash: H256::from([9; 32]), + transactions: vec![], + }; + let probe = EvmBlockWithTxs { + number: U64::from(90), + hash: H256::from([8; 32]), + transactions: vec![tx], + }; + let finality_head = EvmBlock { + number: U64::from(100), + hash: H256::from([9; 32]), + }; + let canonical = EvmBlock { + number: U64::from(50), + hash: block_hash, + }; + let client = SequentialMockClient::new(vec![ + json(U64::from(8453)), + json(&head), + json(&probe), + json(&receipt), + json(&finality_head), + json(&canonical), + ]); + + // When + let result = check_evm::(client, BASE_MAINNET_CHAIN_ID).await; + + // Then + result.unwrap(); + } + + #[tokio::test] + async fn check_evm__should_fail_when_chain_id_differs() { + // Given a provider reporting a different network's chain id. + let client = SequentialMockClient::new(vec![json(U64::from(1))]); + + // When + let result = check_evm::(client, BASE_MAINNET_CHAIN_ID).await; + + // Then + assert_matches!( + result.unwrap_err().downcast_ref::(), + Some(Mismatch::ChainId { .. }) + ); + } + + #[tokio::test] + async fn check_evm__should_walk_back_when_the_probe_block_is_empty() { + // Given the right network, an empty probe block, and a verifiable transaction in the + // block before it. + let tx = H256::from([3; 32]); + let block_hash = H256::from([11; 32]); + let receipt = EvmReceipt { + transaction_hash: tx, + block_hash, + block_number: U64::from(50), + status: U64::from(1), + logs: vec![], + }; + let head = EvmBlockWithTxs { + number: U64::from(100), + hash: H256::from([9; 32]), + transactions: vec![], + }; + let empty_probe = EvmBlockWithTxs { + number: U64::from(90), + hash: H256::from([8; 32]), + transactions: vec![], + }; + let earlier = EvmBlockWithTxs { + number: U64::from(89), + hash: H256::from([7; 32]), + transactions: vec![tx], + }; + let finality_head = EvmBlock { + number: U64::from(100), + hash: H256::from([9; 32]), + }; + let canonical = EvmBlock { + number: U64::from(50), + hash: block_hash, + }; + let client = SequentialMockClient::new(vec![ + json(U64::from(8453)), + json(&head), + json(&empty_probe), + json(&earlier), + json(&receipt), + json(&finality_head), + json(&canonical), + ]); + + // When + let result = check_evm::(client, BASE_MAINNET_CHAIN_ID).await; + + // Then + result.unwrap(); + } + + #[tokio::test] + async fn check_evm__should_fail_when_no_recent_block_carries_transactions() { + // Given the right network but only empty blocks within the walk-back limit. + let head = EvmBlockWithTxs { + number: U64::from(100), + hash: H256::from([9; 32]), + transactions: vec![], + }; + let empty_blocks = (0..=EMPTY_BLOCK_WALKBACK_LIMIT).map(|i| { + json(EvmBlockWithTxs { + number: U64::from(90 - i), + hash: H256::from([8; 32]), + transactions: vec![], + }) + }); + let responses = [json(U64::from(8453)), json(&head)] + .into_iter() + .chain(empty_blocks) + .collect(); + let client = SequentialMockClient::new(responses); + + // When + let result = check_evm::(client, BASE_MAINNET_CHAIN_ID).await; + + // Then + let error = format!("{:#}", result.unwrap_err()); + assert!(error.contains("no transactions"), "{error}"); + } + fn golden_aptos_body(tx: &str, type_tag: &str, sequence_number: u64) -> serde_json::Value { serde_json::json!({ "type": "block_metadata_transaction", diff --git a/crates/foreign-chain-health-check/src/golden.rs b/crates/foreign-chain-health-check/src/golden.rs index 1b9dd16548..c090749598 100644 --- a/crates/foreign-chain-health-check/src/golden.rs +++ b/crates/foreign-chain-health-check/src/golden.rs @@ -1,8 +1,8 @@ //! Per-network golden transactions for the chains still probed against a pinned //! reference, plus decoding helpers. A mainnet transaction does not exist on testnet //! (and vice versa), so the vectors are network-specific; `None` means the chain is -//! skipped. Identity-probed chains (Sui, Starknet) carry no built-in reference — their -//! expected identities come from configuration. +//! skipped. Identity-probed chains (Sui, Starknet, the EVM chains) carry no built-in +//! reference — their expected identities come from configuration. use anyhow::Context; @@ -23,12 +23,6 @@ pub struct AptosVector { } pub struct GoldenSet { - pub base: Option, - pub bnb: Option, - pub arbitrum: Option, - pub polygon: Option, - pub hyper_evm: Option, - pub abstract_chain: Option, pub bitcoin: Option, pub aptos: Option, } @@ -41,30 +35,6 @@ pub fn golden_set(network: Network) -> GoldenSet { } const MAINNET: GoldenSet = GoldenSet { - base: Some(BlockHashVector { - tx: "a11eaa1236e80f26ddc7aca164f2ba4c6c2726405cb12b1aa8f52c520bad99e1", - block_hash: "b8488c9272c547c45e63ea76cc2d1c927c8f888e2721f790b14db996b6cc6aca", - }), - bnb: Some(BlockHashVector { - tx: "90514fff1563dc9876bc9a02a7b1d4dd2ce44b8d11ea0490aa8d427166eba349", - block_hash: "4f125b8e2716df5cbc72719212d5189dae0e49b6b7a44523165cb01888914999", - }), - arbitrum: Some(BlockHashVector { - tx: "8f1f497285dcf54624cba2c3dd46d13e25fc83466033c139e77e4dce12a1e484", - block_hash: "da0e369bfb9688ca4591604104e4f2953329542bfb3bc0d0c94686b5ad798c1c", - }), - polygon: Some(BlockHashVector { - tx: "7b231f0f5bf36782a48db9b1d89e4613bd00618f03c3c0fba922aa59288e4d38", - block_hash: "56d98f80b91c9cf9dcda71c63c01ea441d46ba31149c902adfbee97e55ff82a6", - }), - hyper_evm: Some(BlockHashVector { - tx: "4d94e2c9c33c533f125bd28a788e80ee24c108356e8fa8a7878f642cf94dcf4a", - block_hash: "657b2ee81add87e3f654840425baca06a06d5876f6d2d873197e70f00f6762e6", - }), - abstract_chain: Some(BlockHashVector { - tx: "4572b72d765f07712cf571993fd805888ede9cd05107f65338defee02f7ea755", - block_hash: "3bb255d468a552a75fc3f4916623b207ceb2d3074dfa14442ac03f0f73423708", - }), bitcoin: Some(BlockHashVector { tx: "58ee376171bcc4e2cc040c13848d420b5eaf2f634872055b0a08c1fc2ec6453c", block_hash: "00000000000000000001fadaf3f8591e071c202762193cf78e389ea691f2ecab", @@ -77,15 +47,6 @@ const MAINNET: GoldenSet = GoldenSet { }; const TESTNET: GoldenSet = GoldenSet { - base: None, - bnb: None, - arbitrum: None, - polygon: None, - hyper_evm: None, - abstract_chain: Some(BlockHashVector { - tx: "497fc5f5b5d81d6bc15cccc6d4d8be8ef6ad19376233b944a60dc435593f7234", - block_hash: "4c93dd4a8f347e6480b0a44f8c2b7eecdfb31d711e8d542fd60112ea5d98fb02", - }), bitcoin: Some(BlockHashVector { tx: "5acaa0890f8c1f1b2ac114c25b38d376f23beda1b59e9bcba33256d6e11d7e8e", block_hash: "000000000000021f43445ab447b3fc85e93eca26b56a4f23ef6c017682038ca2", @@ -106,6 +67,17 @@ pub fn hex32(hex: &str) -> anyhow::Result<[u8; 32]> { .map_err(|b: Vec| anyhow::anyhow!("expected 32 bytes, got {}: {hex}", b.len())) } +/// Parse an EVM chain id, accepting decimal (`8453`) or `0x`-hex (`0x2105`). +pub fn chain_id_u64(s: &str) -> anyhow::Result { + let s = s.trim(); + match s.strip_prefix("0x") { + Some(hex) => { + u64::from_str_radix(hex, 16).with_context(|| format!("invalid hex chain id: {s}")) + } + None => s.parse().with_context(|| format!("invalid chain id: {s}")), + } +} + /// Decode a Starknet felt (`0x`-prefixed, possibly fewer than 64 hex digits) into /// a left-zero-padded 32-byte array. pub fn felt32(felt: &str) -> anyhow::Result<[u8; 32]> { @@ -165,18 +137,7 @@ mod tests { // Given / When / Then for network in [Network::Mainnet, Network::Testnet] { let set = golden_set(network); - for v in [ - set.base, - set.bnb, - set.arbitrum, - set.polygon, - set.hyper_evm, - set.abstract_chain, - set.bitcoin, - ] - .into_iter() - .flatten() - { + if let Some(v) = set.bitcoin { hex32(v.tx).unwrap(); hex32(v.block_hash).unwrap(); } diff --git a/crates/foreign-chain-health-check/src/lib.rs b/crates/foreign-chain-health-check/src/lib.rs index efb38d20ce..2a626b124f 100644 --- a/crates/foreign-chain-health-check/src/lib.rs +++ b/crates/foreign-chain-health-check/src/lib.rs @@ -35,6 +35,13 @@ use crate::golden::{AptosVector, BlockHashVector}; #[derive(Debug, Default, serde::Deserialize)] #[serde(default, deny_unknown_fields)] pub struct ExpectedIdentities { + pub base: Option, + pub bnb: Option, + pub arbitrum: Option, + pub polygon: Option, + pub hyper_evm: Option, + #[serde(rename = "abstract")] + pub abstract_chain: Option, pub starknet: Option, pub sui: Option, } @@ -54,32 +61,38 @@ pub async fn check_all_providers( let mut out = Vec::new(); if let Some(cfg) = &fc.base { - run_evm::("base", cfg, golden.base, network, &mut out).await; + run_evm::("base", cfg, identities.base.as_deref(), &mut out).await; } else { mark_not_configured("base", &mut out); } if let Some(cfg) = &fc.bnb { - run_evm::("bnb", cfg, golden.bnb, network, &mut out).await; + run_evm::("bnb", cfg, identities.bnb.as_deref(), &mut out).await; } else { mark_not_configured("bnb", &mut out); } if let Some(cfg) = &fc.arbitrum { - run_evm::("arbitrum", cfg, golden.arbitrum, network, &mut out).await; + run_evm::("arbitrum", cfg, identities.arbitrum.as_deref(), &mut out).await; } else { mark_not_configured("arbitrum", &mut out); } if let Some(cfg) = &fc.polygon { - run_evm::("polygon", cfg, golden.polygon, network, &mut out).await; + run_evm::("polygon", cfg, identities.polygon.as_deref(), &mut out).await; } else { mark_not_configured("polygon", &mut out); } if let Some(cfg) = &fc.hyper_evm { - run_evm::("hyper_evm", cfg, golden.hyper_evm, network, &mut out).await; + run_evm::("hyper_evm", cfg, identities.hyper_evm.as_deref(), &mut out).await; } else { mark_not_configured("hyper_evm", &mut out); } if let Some(cfg) = &fc.abstract_chain { - run_evm::("abstract", cfg, golden.abstract_chain, network, &mut out).await; + run_evm::( + "abstract", + cfg, + identities.abstract_chain.as_deref(), + &mut out, + ) + .await; } else { mark_not_configured("abstract", &mut out); } @@ -163,24 +176,18 @@ async fn run_check(timeout: Duration, fut: impl Future( chain: &'static str, cfg: &ForeignChainConfig, - vector: Option, - network: Network, + expected_chain_id: Option<&str>, out: &mut Vec, ) { - let Some(vector) = vector else { - mark_skipped(chain, cfg, &no_reference_reason(network), out); + let Some(expected) = expected_chain_id else { + mark_missing_identity(chain, cfg, out); return; }; let timeout = timeout_of(cfg); - let parsed = - golden::hex32(vector.tx).and_then(|tx| golden::hex32(vector.block_hash).map(|bh| (tx, bh))); for (name, provider) in cfg.providers.iter() { - let status = match (&parsed, prepare_jsonrpc(provider)) { - (Err(e), _) => Status::Failed(format!("invalid golden vector: {e:#}")), - (Ok(_), Err(e)) => Status::Failed(format!("{e:#}")), - (Ok((tx, bh)), Ok(client)) => { - run_check(timeout, checks::check_evm::(client, *tx, *bh)).await - } + let status = match prepare_jsonrpc(provider) { + Err(e) => Status::Failed(format!("{e:#}")), + Ok(client) => run_check(timeout, checks::check_evm::(client, expected)).await, }; out.push(ProviderResult { chain, @@ -488,10 +495,13 @@ mod tests { base: Some(config_with_provider(auth)), ..Default::default() }; + let identities = ExpectedIdentities { + base: Some("8453".to_string()), + ..Default::default() + }; // When - let results = - check_all_providers(&fc, Network::Mainnet, &ExpectedIdentities::default()).await; + let results = check_all_providers(&fc, Network::Mainnet, &identities).await; // Then assert_eq!(results[0].chain, "base"); diff --git a/crates/foreign-chain-inspector/src/evm/inspector.rs b/crates/foreign-chain-inspector/src/evm/inspector.rs index f7f8caf219..74e389ffb1 100644 --- a/crates/foreign-chain-inspector/src/evm/inspector.rs +++ b/crates/foreign-chain-inspector/src/evm/inspector.rs @@ -6,13 +6,14 @@ use jsonrpsee::core::client::ClientT; use crate::{EthereumFinality, ForeignChainInspectionError, ForeignChainInspector}; use foreign_chain_rpc_interfaces::evm::{ - BlockNumberOrTag, FinalityTag, GetBlockByNumberArgs, GetBlockByNumberResponse, - GetTransactionReceiptARgs, GetTransactionReceiptResponse, H256, Log, - ReturnFullTransactionObjects, U64, + BlockNumberOrTag, ChainIdArgs, FinalityTag, GetBlockByNumberArgs, GetBlockByNumberResponse, + GetBlockByNumberWithTxsResponse, GetTransactionReceiptARgs, GetTransactionReceiptResponse, + H256, Log, ReturnFullTransactionObjects, U64, }; const GET_TRANSACTION_RECEIPT_METHOD: &str = "eth_getTransactionReceipt"; const GET_BLOCK_BY_NUMBER_METHOD: &str = "eth_getBlockByNumber"; +const CHAIN_ID_METHOD: &str = "eth_chainId"; /// Marker trait for EVM-compatible chain type parameters. /// @@ -106,6 +107,26 @@ where } } + /// The provider's chain id (`eth_chainId`). Identifies which network a provider serves + /// without depending on historical data. + pub async fn chain_id(&self) -> Result { + let id: U64 = self.client.request(CHAIN_ID_METHOD, &ChainIdArgs).await?; + Ok(id.as_u64()) + } + + /// Fetches a block (and its transaction hashes) by number or finality tag. The health + /// probe reads a recent, unpruned transaction from it to exercise the extraction pipeline. + pub async fn block_with_txs( + &self, + block: BlockNumberOrTag, + ) -> Result { + let args = GetBlockByNumberArgs::new(block, ReturnFullTransactionObjects::from(false)); + Ok(self + .client + .request(GET_BLOCK_BY_NUMBER_METHOD, &args) + .await?) + } + /// Checks that the receipt's block has reached the requested finality level — i.e. that the /// head of the chain at `finality` is at or past `receipt_block_number`. async fn verify_finality_level( diff --git a/crates/foreign-chain-rpc-interfaces/src/evm.rs b/crates/foreign-chain-rpc-interfaces/src/evm.rs index d7c364d0d7..a007f86750 100644 --- a/crates/foreign-chain-rpc-interfaces/src/evm.rs +++ b/crates/foreign-chain-rpc-interfaces/src/evm.rs @@ -34,6 +34,18 @@ pub struct GetBlockByNumberResponse { pub hash: H256, } +/// Like [`GetBlockByNumberResponse`] but also captures the block's transaction hashes +/// (`eth_getBlockByNumber` with `full = false` returns `transactions` as an array of hashes). +/// Used by the health probe to discover a recent transaction to exercise the inspector against; +/// kept separate so the finality/canonical checks keep deserializing the leaner response. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GetBlockByNumberWithTxsResponse { + pub number: U64, + pub hash: H256, + #[serde(default)] + pub transactions: Vec, +} + /// Partial RPC arguments for `eth_getBlockByNumber`. /// #[derive( @@ -107,3 +119,12 @@ impl ToRpcParams for &GetTransactionReceiptARgs { impl ToRpcParams for &GetBlockByNumberArgs { to_rpc_params_impl!(); } + +/// `eth_chainId` takes no parameters. +pub struct ChainIdArgs; + +impl ToRpcParams for &ChainIdArgs { + fn to_rpc_params(self) -> Result>, serde_json::Error> { + Ok(None) + } +} diff --git a/deployment/cvm-deployment/user-config.toml b/deployment/cvm-deployment/user-config.toml index e2f69d0250..75366c85f7 100644 --- a/deployment/cvm-deployment/user-config.toml +++ b/deployment/cvm-deployment/user-config.toml @@ -159,5 +159,6 @@ rpc_url = "https://fullnode.testnet.sui.io:443" # 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] +abstract = "11124" # mainnet: "2741" starknet = "0x534e5f5345504f4c4941" # mainnet: "0x534e5f4d41494e" sui = "69WiPg3DAQiwdxfncX6wYQ2siKwAe6L9BZthQea3JNMD" # mainnet: "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S" diff --git a/docs/localnet/mpc-config.template.toml b/docs/localnet/mpc-config.template.toml index b2483ae8eb..6b07b27f1d 100644 --- a/docs/localnet/mpc-config.template.toml +++ b/docs/localnet/mpc-config.template.toml @@ -106,5 +106,6 @@ rpc_url = "https://archive.mainnet.sui.io" kind = "none" [node.foreign_chain_health_check.identities] +abstract = "11124" starknet = "0x534e5f4d41494e" sui = "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S" diff --git a/docs/localnet/mpc-configs/config.yaml.template b/docs/localnet/mpc-configs/config.yaml.template index d558f650e6..d3b590d2c7 100644 --- a/docs/localnet/mpc-configs/config.yaml.template +++ b/docs/localnet/mpc-configs/config.yaml.template @@ -71,5 +71,6 @@ foreign_chains: kind: none foreign_chain_health_check: identities: + abstract: "11124" starknet: "0x534e5f4d41494e" sui: "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S"