1use crate::abi::EthBridgeConfig;
5use crate::crypto::BridgeAuthorityKeyPair;
6use crate::error::BridgeError;
7use crate::eth_client::EthClient;
8use crate::metered_eth_provider::new_metered_eth_multi_provider;
9use crate::metrics::BridgeMetrics;
10use crate::sui_client::SuiBridgeClient;
11use crate::types::{BridgeAction, is_route_valid};
12use crate::utils::get_eth_contract_addresses;
13use alloy::primitives::Address as EthAddress;
14use alloy::providers::Provider;
15use anyhow::anyhow;
16use futures::StreamExt;
17use serde::{Deserialize, Serialize};
18use serde_with::serde_as;
19use std::collections::BTreeMap;
20use std::collections::HashSet;
21use std::path::PathBuf;
22use std::str::FromStr;
23use std::sync::Arc;
24use sui_config::Config;
25use sui_keys::keypair_file::read_key;
26use sui_types::base_types::ObjectRef;
27use sui_types::base_types::{ObjectID, SuiAddress};
28use sui_types::bridge::BridgeChainId;
29use sui_types::crypto::KeypairTraits;
30use sui_types::crypto::{NetworkKeyPair, SuiKeyPair, get_key_pair_from_rng};
31use sui_types::digests::{get_mainnet_chain_identifier, get_testnet_chain_identifier};
32use sui_types::event::EventID;
33use sui_types::gas_coin::GasCoin;
34use sui_types::object::Owner;
35use tracing::info;
36
37#[serde_as]
38#[derive(Clone, Debug, Deserialize, Serialize)]
39#[serde(rename_all = "kebab-case")]
40pub struct EthConfig {
41 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub eth_rpc_url: Option<String>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub eth_rpc_urls: Option<Vec<String>>,
49 #[serde(default = "default_quorum")]
51 pub eth_rpc_quorum: usize,
52 #[serde(default = "default_health_check_interval_secs")]
54 pub eth_health_check_interval_secs: u64,
55 pub eth_bridge_proxy_address: String,
57 pub eth_bridge_chain_id: u8,
59 pub eth_contracts_start_block_fallback: Option<u64>,
67 #[serde(skip_serializing_if = "Option::is_none")]
73 pub eth_contracts_start_block_override: Option<u64>,
74}
75
76fn default_quorum() -> usize {
77 1
78}
79
80fn default_health_check_interval_secs() -> u64 {
81 300 }
83
84impl EthConfig {
85 pub fn rpc_urls(&self) -> Vec<String> {
87 if let Some(ref urls) = self.eth_rpc_urls {
88 urls.clone()
89 } else if let Some(ref url) = self.eth_rpc_url {
90 vec![url.clone()]
91 } else {
92 vec![]
93 }
94 }
95}
96
97#[serde_as]
98#[derive(Clone, Debug, Deserialize, Serialize)]
99#[serde(rename_all = "kebab-case")]
100pub struct SuiConfig {
101 pub sui_rpc_url: String,
103 pub sui_bridge_chain_id: u8,
105 #[serde(skip_serializing_if = "Option::is_none")]
108 pub bridge_client_key_path: Option<PathBuf>,
109 #[serde(skip_serializing_if = "Option::is_none")]
114 pub bridge_client_gas_object: Option<ObjectID>,
115 #[serde(skip_serializing_if = "Option::is_none")]
123 pub sui_bridge_module_last_processed_event_id_override: Option<EventID>,
124 #[serde(skip_serializing_if = "Option::is_none")]
128 pub sui_bridge_next_sequence_number_override: Option<u64>,
129}
130
131#[serde_as]
132#[derive(Debug, Deserialize, Serialize)]
133#[serde(rename_all = "kebab-case")]
134pub struct BridgeNodeConfig {
135 pub server_listen_port: u16,
137 pub metrics_port: u16,
139 pub bridge_authority_key_path: PathBuf,
141 pub run_client: bool,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub db_path: Option<PathBuf>,
147 pub approved_governance_actions: Vec<BridgeAction>,
149 pub sui: SuiConfig,
151 pub eth: EthConfig,
153 #[serde(default = "default_ed25519_key_pair")]
155 pub metrics_key_pair: NetworkKeyPair,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub metrics: Option<MetricsConfig>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub watchdog_config: Option<WatchdogConfig>,
161}
162
163pub fn default_ed25519_key_pair() -> NetworkKeyPair {
164 get_key_pair_from_rng(&mut rand::rngs::OsRng).1
165}
166
167#[derive(Debug, Clone, Deserialize, Serialize)]
168#[serde(rename_all = "kebab-case")]
169pub struct MetricsConfig {
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub push_interval_seconds: Option<u64>,
172 pub push_url: String,
173}
174
175#[derive(Debug, Clone, Deserialize, Serialize)]
176#[serde(rename_all = "kebab-case")]
177pub struct WatchdogConfig {
178 pub total_supplies: BTreeMap<String, String>,
180}
181
182impl Config for BridgeNodeConfig {}
183
184impl BridgeNodeConfig {
185 pub async fn validate(
186 &self,
187 metrics: Arc<BridgeMetrics>,
188 ) -> anyhow::Result<(BridgeServerConfig, Option<BridgeClientConfig>)> {
189 info!("Starting config validation");
190 if !is_route_valid(
191 BridgeChainId::try_from(self.sui.sui_bridge_chain_id)?,
192 BridgeChainId::try_from(self.eth.eth_bridge_chain_id)?,
193 ) {
194 return Err(anyhow!(
195 "Route between Sui chain id {} and Eth chain id {} is not valid",
196 self.sui.sui_bridge_chain_id,
197 self.eth.eth_bridge_chain_id,
198 ));
199 };
200
201 let bridge_authority_key = match read_key(&self.bridge_authority_key_path, true)? {
202 SuiKeyPair::Secp256k1(key) => key,
203 _ => unreachable!("we required secp256k1 key in `read_key`"),
204 };
205
206 let sui_client =
209 Arc::new(SuiBridgeClient::new(&self.sui.sui_rpc_url, metrics.clone()).await?);
210 let bridge_committee = sui_client
211 .get_bridge_committee()
212 .await
213 .map_err(|e| anyhow!("Error getting bridge committee: {:?}", e))?;
214 if !bridge_committee.is_active_member(&bridge_authority_key.public().into()) {
215 return Err(anyhow!(
216 "Bridge authority key is not part of bridge committee"
217 ));
218 }
219
220 let (eth_client, eth_contracts) = self.prepare_for_eth(metrics.clone()).await?;
221 let bridge_summary = sui_client
222 .get_bridge_summary()
223 .await
224 .map_err(|e| anyhow!("Error getting bridge summary: {:?}", e))?;
225 if bridge_summary.chain_id != self.sui.sui_bridge_chain_id {
226 anyhow::bail!(
227 "Bridge chain id mismatch: expected {}, but connected to {}",
228 self.sui.sui_bridge_chain_id,
229 bridge_summary.chain_id
230 );
231 }
232
233 for action in &self.approved_governance_actions {
235 if !action.is_governance_action() {
236 anyhow::bail!(format!(
237 "{:?}",
238 BridgeError::ActionIsNotGovernanceAction(Box::new(action.clone()))
239 ));
240 }
241 }
242 let approved_governance_actions = self.approved_governance_actions.clone();
243
244 let bridge_server_config = BridgeServerConfig {
245 key: bridge_authority_key,
246 metrics_port: self.metrics_port,
247 eth_bridge_proxy_address: eth_contracts[0], server_listen_port: self.server_listen_port,
249 sui_client: sui_client.clone(),
250 eth_client: eth_client.clone(),
251 approved_governance_actions,
252 };
253 if !self.run_client {
254 return Ok((bridge_server_config, None));
255 }
256
257 let (bridge_client_key, client_sui_address, gas_object_ref) =
259 self.prepare_for_sui(sui_client.clone(), metrics).await?;
260
261 let db_path = self
262 .db_path
263 .clone()
264 .ok_or(anyhow!("`db_path` is required when `run_client` is true"))?;
265
266 let bridge_client_config = BridgeClientConfig {
267 sui_address: client_sui_address,
268 key: bridge_client_key,
269 gas_object_ref,
270 metrics_port: self.metrics_port,
271 sui_client: sui_client.clone(),
272 eth_client: eth_client.clone(),
273 db_path,
274 eth_contracts,
275 eth_contracts_start_block_fallback: self
277 .eth
278 .eth_contracts_start_block_fallback
279 .unwrap(),
280 eth_contracts_start_block_override: self.eth.eth_contracts_start_block_override,
281 sui_bridge_module_last_processed_event_id_override: self
282 .sui
283 .sui_bridge_module_last_processed_event_id_override,
284 sui_bridge_next_sequence_number_override: self
285 .sui
286 .sui_bridge_next_sequence_number_override,
287 sui_bridge_chain_id: self.sui.sui_bridge_chain_id,
288 };
289
290 info!("Config validation complete");
291 Ok((bridge_server_config, Some(bridge_client_config)))
292 }
293
294 async fn prepare_for_eth(
295 &self,
296 metrics: Arc<BridgeMetrics>,
297 ) -> anyhow::Result<(Arc<EthClient>, Vec<EthAddress>)> {
298 info!("Creating Ethereum client provider");
299 let bridge_proxy_address = EthAddress::from_str(&self.eth.eth_bridge_proxy_address)?;
300 let rpc_urls = self.eth.rpc_urls();
301 anyhow::ensure!(
302 !rpc_urls.is_empty(),
303 "At least one Ethereum RPC URL must be provided"
304 );
305
306 let provider = new_metered_eth_multi_provider(
307 rpc_urls.clone(),
308 self.eth.eth_rpc_quorum,
309 self.eth.eth_health_check_interval_secs,
310 metrics.clone(),
311 )
312 .await?;
313
314 let chain_id = provider.get_chain_id().await?;
315 let (
316 committee_address,
317 limiter_address,
318 vault_address,
319 config_address,
320 _weth_address,
321 _usdt_address,
322 _wbtc_address,
323 _lbtc_address,
324 ) = get_eth_contract_addresses(bridge_proxy_address, provider.clone()).await?;
325 let config = EthBridgeConfig::new(config_address, provider.clone());
326
327 if self.run_client && self.eth.eth_contracts_start_block_fallback.is_none() {
328 return Err(anyhow!(
329 "eth_contracts_start_block_fallback is required when run_client is true"
330 ));
331 }
332
333 let bridge_chain_id: u8 = config.chainID().call().await?;
336 if self.eth.eth_bridge_chain_id != bridge_chain_id {
337 return Err(anyhow!(
338 "Bridge chain id mismatch: expected {}, but connected to {}",
339 self.eth.eth_bridge_chain_id,
340 bridge_chain_id
341 ));
342 }
343 if bridge_chain_id == BridgeChainId::EthMainnet as u8 && chain_id != 1 {
344 anyhow::bail!("Expected Eth chain id 1, but connected to {}", chain_id);
345 }
346 if bridge_chain_id == BridgeChainId::EthSepolia as u8 && chain_id != 11155111 {
347 anyhow::bail!(
348 "Expected Eth chain id 11155111, but connected to {}",
349 chain_id
350 );
351 }
352 info!(
353 "Connected to Eth chain: {}, Bridge chain id: {}",
354 chain_id, bridge_chain_id,
355 );
356
357 let all_addresses = vec![
359 bridge_proxy_address,
360 committee_address,
361 config_address,
362 limiter_address,
363 vault_address,
364 ];
365 let valid_addresses: Vec<_> = all_addresses
366 .into_iter()
367 .filter(|addr| !addr.is_zero())
368 .collect();
369
370 if valid_addresses.len() < 5 {
371 tracing::warn!(
372 "Some contract addresses are zero - likely storage layout mismatch. \
373 Event watching will be limited. Valid addresses: {:?}",
374 valid_addresses
375 );
376 }
377
378 let eth_client = Arc::new(
379 EthClient::from_provider(provider, HashSet::from_iter(valid_addresses.clone())).await?,
380 );
381 info!("Ethereum client setup complete");
382 Ok((eth_client, valid_addresses))
383 }
384
385 async fn prepare_for_sui(
386 &self,
387 sui_client: Arc<SuiBridgeClient>,
388 metrics: Arc<BridgeMetrics>,
389 ) -> anyhow::Result<(SuiKeyPair, SuiAddress, ObjectRef)> {
390 let bridge_client_key = match &self.sui.bridge_client_key_path {
391 None => read_key(&self.bridge_authority_key_path, true),
392 Some(path) => read_key(path, false),
393 }?;
394
395 let sui_identifier = sui_client
398 .get_chain_identifier()
399 .await
400 .map_err(|e| anyhow!("Error getting chain identifier from Sui: {:?}", e))?;
401 if self.sui.sui_bridge_chain_id == BridgeChainId::SuiMainnet as u8
402 && sui_identifier != get_mainnet_chain_identifier().to_string()
403 {
404 anyhow::bail!(
405 "Expected sui chain identifier {}, but connected to {}",
406 self.sui.sui_bridge_chain_id,
407 sui_identifier
408 );
409 }
410 if self.sui.sui_bridge_chain_id == BridgeChainId::SuiTestnet as u8
411 && sui_identifier != get_testnet_chain_identifier().to_string()
412 {
413 anyhow::bail!(
414 "Expected sui chain identifier {}, but connected to {}",
415 self.sui.sui_bridge_chain_id,
416 sui_identifier
417 );
418 }
419 info!(
420 "Connected to Sui chain: {}, Bridge chain id: {}",
421 sui_identifier, self.sui.sui_bridge_chain_id,
422 );
423
424 let client_sui_address = SuiAddress::from(&bridge_client_key.public());
425
426 let gas_object_id = match self.sui.bridge_client_gas_object {
427 Some(id) => id,
428 None => {
429 info!("No gas object configured, finding gas object with highest balance");
430 let sui_client = sui_rpc_api::Client::new(&self.sui.sui_rpc_url)?;
431 pick_highest_balance_coin(sui_client, client_sui_address, 10_000_000_000).await?
433 }
434 };
435 let (gas_coin, gas_object_ref, owner) = sui_client
436 .get_gas_data_panic_if_not_gas(gas_object_id)
437 .await;
438 if owner != Owner::AddressOwner(client_sui_address) {
439 return Err(anyhow!(
440 "Gas object {:?} is not owned by bridge client key's associated sui address {:?}, but {:?}",
441 gas_object_id,
442 client_sui_address,
443 owner
444 ));
445 }
446 let balance = gas_coin.value();
447 info!("Gas object balance: {}", balance);
448 metrics.gas_coin_balance.set(balance as i64);
449
450 info!("Sui client setup complete");
451 Ok((bridge_client_key, client_sui_address, gas_object_ref))
452 }
453}
454
455pub struct BridgeServerConfig {
456 pub key: BridgeAuthorityKeyPair,
457 pub server_listen_port: u16,
458 pub eth_bridge_proxy_address: EthAddress,
459 pub metrics_port: u16,
460 pub sui_client: Arc<SuiBridgeClient>,
461 pub eth_client: Arc<EthClient>,
462 pub approved_governance_actions: Vec<BridgeAction>,
464}
465
466pub struct BridgeClientConfig {
467 pub sui_address: SuiAddress,
468 pub key: SuiKeyPair,
469 pub gas_object_ref: ObjectRef,
470 pub metrics_port: u16,
471 pub sui_client: Arc<SuiBridgeClient>,
472 pub eth_client: Arc<EthClient>,
473 pub db_path: PathBuf,
474 pub eth_contracts: Vec<EthAddress>,
475 pub eth_contracts_start_block_fallback: u64,
477 pub eth_contracts_start_block_override: Option<u64>,
478 pub sui_bridge_module_last_processed_event_id_override: Option<EventID>,
479 pub sui_bridge_next_sequence_number_override: Option<u64>,
480 pub sui_bridge_chain_id: u8,
481}
482
483#[serde_as]
484#[derive(Clone, Debug, Deserialize, Serialize)]
485#[serde(rename_all = "kebab-case")]
486pub struct BridgeCommitteeConfig {
487 pub bridge_authority_port_and_key_path: Vec<(u64, PathBuf)>,
488}
489
490impl Config for BridgeCommitteeConfig {}
491
492pub async fn pick_highest_balance_coin(
493 client: sui_rpc_api::Client,
494 address: SuiAddress,
495 minimal_amount: u64,
496) -> anyhow::Result<ObjectID> {
497 info!("Looking for a suitable gas coin for address {:?}", address);
498
499 let mut stream = client
501 .list_owned_objects(address, Some(GasCoin::type_()))
502 .boxed();
503
504 let mut coins_checked = 0;
505
506 while let Some(Ok(object)) = stream.next().await {
507 let Ok(coin) = GasCoin::try_from(&object) else {
508 continue;
509 };
510 info!(
511 "Checking coin: {:?}, balance: {}",
512 object.id(),
513 coin.value()
514 );
515 coins_checked += 1;
516
517 if coin.value() >= minimal_amount {
519 info!(
520 "Found suitable gas coin with {} mist (object ID: {:?})",
521 coin.value(),
522 object.id(),
523 );
524 return Ok(object.id());
525 }
526
527 if coins_checked >= 1000 {
529 break;
530 }
531 }
532
533 Err(anyhow!(
534 "No suitable gas coin with >= {} mist found for address {:?} after checking {} coins",
535 minimal_amount,
536 address,
537 coins_checked
538 ))
539}
540
541#[derive(Debug, Eq, PartialEq, Clone)]
542pub struct EthContractAddresses {
543 pub sui_bridge: EthAddress,
544 pub bridge_committee: EthAddress,
545 pub bridge_config: EthAddress,
546 pub bridge_limiter: EthAddress,
547 pub bridge_vault: EthAddress,
548}