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_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 pub eth_rpc_url: String,
43 pub eth_bridge_proxy_address: String,
45 pub eth_bridge_chain_id: u8,
47 pub eth_contracts_start_block_fallback: Option<u64>,
55 #[serde(skip_serializing_if = "Option::is_none")]
61 pub eth_contracts_start_block_override: Option<u64>,
62}
63
64#[serde_as]
65#[derive(Clone, Debug, Deserialize, Serialize)]
66#[serde(rename_all = "kebab-case")]
67pub struct SuiConfig {
68 pub sui_rpc_url: String,
70 pub sui_bridge_chain_id: u8,
72 #[serde(skip_serializing_if = "Option::is_none")]
75 pub bridge_client_key_path: Option<PathBuf>,
76 #[serde(skip_serializing_if = "Option::is_none")]
81 pub bridge_client_gas_object: Option<ObjectID>,
82 #[serde(skip_serializing_if = "Option::is_none")]
90 pub sui_bridge_module_last_processed_event_id_override: Option<EventID>,
91 #[serde(skip_serializing_if = "Option::is_none")]
95 pub sui_bridge_next_sequence_number_override: Option<u64>,
96}
97
98#[serde_as]
99#[derive(Debug, Deserialize, Serialize)]
100#[serde(rename_all = "kebab-case")]
101pub struct BridgeNodeConfig {
102 pub server_listen_port: u16,
104 pub metrics_port: u16,
106 pub bridge_authority_key_path: PathBuf,
108 pub run_client: bool,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub db_path: Option<PathBuf>,
114 pub approved_governance_actions: Vec<BridgeAction>,
116 pub sui: SuiConfig,
118 pub eth: EthConfig,
120 #[serde(default = "default_ed25519_key_pair")]
122 pub metrics_key_pair: NetworkKeyPair,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub metrics: Option<MetricsConfig>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub watchdog_config: Option<WatchdogConfig>,
128}
129
130pub fn default_ed25519_key_pair() -> NetworkKeyPair {
131 get_key_pair_from_rng(&mut rand::rngs::OsRng).1
132}
133
134#[derive(Debug, Clone, Deserialize, Serialize)]
135#[serde(rename_all = "kebab-case")]
136pub struct MetricsConfig {
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub push_interval_seconds: Option<u64>,
139 pub push_url: String,
140}
141
142#[derive(Debug, Clone, Deserialize, Serialize)]
143#[serde(rename_all = "kebab-case")]
144pub struct WatchdogConfig {
145 pub total_supplies: BTreeMap<String, String>,
147}
148
149impl Config for BridgeNodeConfig {}
150
151impl BridgeNodeConfig {
152 pub async fn validate(
153 &self,
154 metrics: Arc<BridgeMetrics>,
155 ) -> anyhow::Result<(BridgeServerConfig, Option<BridgeClientConfig>)> {
156 info!("Starting config validation");
157 if !is_route_valid(
158 BridgeChainId::try_from(self.sui.sui_bridge_chain_id)?,
159 BridgeChainId::try_from(self.eth.eth_bridge_chain_id)?,
160 ) {
161 return Err(anyhow!(
162 "Route between Sui chain id {} and Eth chain id {} is not valid",
163 self.sui.sui_bridge_chain_id,
164 self.eth.eth_bridge_chain_id,
165 ));
166 };
167
168 let bridge_authority_key = match read_key(&self.bridge_authority_key_path, true)? {
169 SuiKeyPair::Secp256k1(key) => key,
170 _ => unreachable!("we required secp256k1 key in `read_key`"),
171 };
172
173 let sui_client =
176 Arc::new(SuiBridgeClient::new(&self.sui.sui_rpc_url, metrics.clone()).await?);
177 let bridge_committee = sui_client
178 .get_bridge_committee()
179 .await
180 .map_err(|e| anyhow!("Error getting bridge committee: {:?}", e))?;
181 if !bridge_committee.is_active_member(&bridge_authority_key.public().into()) {
182 return Err(anyhow!(
183 "Bridge authority key is not part of bridge committee"
184 ));
185 }
186
187 let (eth_client, eth_contracts) = self.prepare_for_eth(metrics.clone()).await?;
188 let bridge_summary = sui_client
189 .get_bridge_summary()
190 .await
191 .map_err(|e| anyhow!("Error getting bridge summary: {:?}", e))?;
192 if bridge_summary.chain_id != self.sui.sui_bridge_chain_id {
193 anyhow::bail!(
194 "Bridge chain id mismatch: expected {}, but connected to {}",
195 self.sui.sui_bridge_chain_id,
196 bridge_summary.chain_id
197 );
198 }
199
200 for action in &self.approved_governance_actions {
202 if !action.is_governance_action() {
203 anyhow::bail!(format!(
204 "{:?}",
205 BridgeError::ActionIsNotGovernanceAction(Box::new(action.clone()))
206 ));
207 }
208 }
209 let approved_governance_actions = self.approved_governance_actions.clone();
210
211 let bridge_server_config = BridgeServerConfig {
212 key: bridge_authority_key,
213 metrics_port: self.metrics_port,
214 eth_bridge_proxy_address: eth_contracts[0], server_listen_port: self.server_listen_port,
216 sui_client: sui_client.clone(),
217 eth_client: eth_client.clone(),
218 approved_governance_actions,
219 };
220 if !self.run_client {
221 return Ok((bridge_server_config, None));
222 }
223
224 let (bridge_client_key, client_sui_address, gas_object_ref) =
226 self.prepare_for_sui(sui_client.clone(), metrics).await?;
227
228 let db_path = self
229 .db_path
230 .clone()
231 .ok_or(anyhow!("`db_path` is required when `run_client` is true"))?;
232
233 let bridge_client_config = BridgeClientConfig {
234 sui_address: client_sui_address,
235 key: bridge_client_key,
236 gas_object_ref,
237 metrics_port: self.metrics_port,
238 sui_client: sui_client.clone(),
239 eth_client: eth_client.clone(),
240 db_path,
241 eth_contracts,
242 eth_contracts_start_block_fallback: self
244 .eth
245 .eth_contracts_start_block_fallback
246 .unwrap(),
247 eth_contracts_start_block_override: self.eth.eth_contracts_start_block_override,
248 sui_bridge_module_last_processed_event_id_override: self
249 .sui
250 .sui_bridge_module_last_processed_event_id_override,
251 sui_bridge_next_sequence_number_override: self
252 .sui
253 .sui_bridge_next_sequence_number_override,
254 sui_bridge_chain_id: self.sui.sui_bridge_chain_id,
255 };
256
257 info!("Config validation complete");
258 Ok((bridge_server_config, Some(bridge_client_config)))
259 }
260
261 async fn prepare_for_eth(
262 &self,
263 metrics: Arc<BridgeMetrics>,
264 ) -> anyhow::Result<(Arc<EthClient>, Vec<EthAddress>)> {
265 info!("Creating Ethereum client provider");
266 let bridge_proxy_address = EthAddress::from_str(&self.eth.eth_bridge_proxy_address)?;
267 let provider = new_metered_eth_provider(&self.eth.eth_rpc_url, metrics.clone()).unwrap();
268 let chain_id = provider.get_chain_id().await?;
269 let (
270 committee_address,
271 limiter_address,
272 vault_address,
273 config_address,
274 _weth_address,
275 _usdt_address,
276 _wbtc_address,
277 _lbtc_address,
278 ) = get_eth_contract_addresses(bridge_proxy_address, provider.clone()).await?;
279 let config = EthBridgeConfig::new(config_address, provider.clone());
280
281 if self.run_client && self.eth.eth_contracts_start_block_fallback.is_none() {
282 return Err(anyhow!(
283 "eth_contracts_start_block_fallback is required when run_client is true"
284 ));
285 }
286
287 let bridge_chain_id: u8 = config.chainID().call().await?;
290 if self.eth.eth_bridge_chain_id != bridge_chain_id {
291 return Err(anyhow!(
292 "Bridge chain id mismatch: expected {}, but connected to {}",
293 self.eth.eth_bridge_chain_id,
294 bridge_chain_id
295 ));
296 }
297 if bridge_chain_id == BridgeChainId::EthMainnet as u8 && chain_id != 1 {
298 anyhow::bail!("Expected Eth chain id 1, but connected to {}", chain_id);
299 }
300 if bridge_chain_id == BridgeChainId::EthSepolia as u8 && chain_id != 11155111 {
301 anyhow::bail!(
302 "Expected Eth chain id 11155111, but connected to {}",
303 chain_id
304 );
305 }
306 info!(
307 "Connected to Eth chain: {}, Bridge chain id: {}",
308 chain_id, bridge_chain_id,
309 );
310
311 let all_addresses = vec![
313 bridge_proxy_address,
314 committee_address,
315 config_address,
316 limiter_address,
317 vault_address,
318 ];
319 let valid_addresses: Vec<_> = all_addresses
320 .into_iter()
321 .filter(|addr| !addr.is_zero())
322 .collect();
323
324 if valid_addresses.len() < 5 {
325 tracing::warn!(
326 "Some contract addresses are zero - likely storage layout mismatch. \
327 Event watching will be limited. Valid addresses: {:?}",
328 valid_addresses
329 );
330 }
331
332 let eth_client = Arc::new(
333 EthClient::new(
334 &self.eth.eth_rpc_url,
335 HashSet::from_iter(valid_addresses.clone()),
336 metrics,
337 )
338 .await?,
339 );
340 info!("Ethereum client setup complete");
341 Ok((eth_client, valid_addresses))
342 }
343
344 async fn prepare_for_sui(
345 &self,
346 sui_client: Arc<SuiBridgeClient>,
347 metrics: Arc<BridgeMetrics>,
348 ) -> anyhow::Result<(SuiKeyPair, SuiAddress, ObjectRef)> {
349 let bridge_client_key = match &self.sui.bridge_client_key_path {
350 None => read_key(&self.bridge_authority_key_path, true),
351 Some(path) => read_key(path, false),
352 }?;
353
354 let sui_identifier = sui_client
357 .get_chain_identifier()
358 .await
359 .map_err(|e| anyhow!("Error getting chain identifier from Sui: {:?}", e))?;
360 if self.sui.sui_bridge_chain_id == BridgeChainId::SuiMainnet as u8
361 && sui_identifier != get_mainnet_chain_identifier().to_string()
362 {
363 anyhow::bail!(
364 "Expected sui chain identifier {}, but connected to {}",
365 self.sui.sui_bridge_chain_id,
366 sui_identifier
367 );
368 }
369 if self.sui.sui_bridge_chain_id == BridgeChainId::SuiTestnet as u8
370 && sui_identifier != get_testnet_chain_identifier().to_string()
371 {
372 anyhow::bail!(
373 "Expected sui chain identifier {}, but connected to {}",
374 self.sui.sui_bridge_chain_id,
375 sui_identifier
376 );
377 }
378 info!(
379 "Connected to Sui chain: {}, Bridge chain id: {}",
380 sui_identifier, self.sui.sui_bridge_chain_id,
381 );
382
383 let client_sui_address = SuiAddress::from(&bridge_client_key.public());
384
385 let gas_object_id = match self.sui.bridge_client_gas_object {
386 Some(id) => id,
387 None => {
388 info!("No gas object configured, finding gas object with highest balance");
389 let sui_client = sui_rpc_api::Client::new(&self.sui.sui_rpc_url)?;
390 pick_highest_balance_coin(sui_client, client_sui_address, 10_000_000_000).await?
392 }
393 };
394 let (gas_coin, gas_object_ref, owner) = sui_client
395 .get_gas_data_panic_if_not_gas(gas_object_id)
396 .await;
397 if owner != Owner::AddressOwner(client_sui_address) {
398 return Err(anyhow!(
399 "Gas object {:?} is not owned by bridge client key's associated sui address {:?}, but {:?}",
400 gas_object_id,
401 client_sui_address,
402 owner
403 ));
404 }
405 let balance = gas_coin.value();
406 info!("Gas object balance: {}", balance);
407 metrics.gas_coin_balance.set(balance as i64);
408
409 info!("Sui client setup complete");
410 Ok((bridge_client_key, client_sui_address, gas_object_ref))
411 }
412}
413
414pub struct BridgeServerConfig {
415 pub key: BridgeAuthorityKeyPair,
416 pub server_listen_port: u16,
417 pub eth_bridge_proxy_address: EthAddress,
418 pub metrics_port: u16,
419 pub sui_client: Arc<SuiBridgeClient>,
420 pub eth_client: Arc<EthClient>,
421 pub approved_governance_actions: Vec<BridgeAction>,
423}
424
425pub struct BridgeClientConfig {
426 pub sui_address: SuiAddress,
427 pub key: SuiKeyPair,
428 pub gas_object_ref: ObjectRef,
429 pub metrics_port: u16,
430 pub sui_client: Arc<SuiBridgeClient>,
431 pub eth_client: Arc<EthClient>,
432 pub db_path: PathBuf,
433 pub eth_contracts: Vec<EthAddress>,
434 pub eth_contracts_start_block_fallback: u64,
436 pub eth_contracts_start_block_override: Option<u64>,
437 pub sui_bridge_module_last_processed_event_id_override: Option<EventID>,
438 pub sui_bridge_next_sequence_number_override: Option<u64>,
439 pub sui_bridge_chain_id: u8,
440}
441
442#[serde_as]
443#[derive(Clone, Debug, Deserialize, Serialize)]
444#[serde(rename_all = "kebab-case")]
445pub struct BridgeCommitteeConfig {
446 pub bridge_authority_port_and_key_path: Vec<(u64, PathBuf)>,
447}
448
449impl Config for BridgeCommitteeConfig {}
450
451pub async fn pick_highest_balance_coin(
452 client: sui_rpc_api::Client,
453 address: SuiAddress,
454 minimal_amount: u64,
455) -> anyhow::Result<ObjectID> {
456 info!("Looking for a suitable gas coin for address {:?}", address);
457
458 let mut stream = client
460 .list_owned_objects(address, Some(GasCoin::type_()))
461 .boxed();
462
463 let mut coins_checked = 0;
464
465 while let Some(Ok(object)) = stream.next().await {
466 let Ok(coin) = GasCoin::try_from(&object) else {
467 continue;
468 };
469 info!(
470 "Checking coin: {:?}, balance: {}",
471 object.id(),
472 coin.value()
473 );
474 coins_checked += 1;
475
476 if coin.value() >= minimal_amount {
478 info!(
479 "Found suitable gas coin with {} mist (object ID: {:?})",
480 coin.value(),
481 object.id(),
482 );
483 return Ok(object.id());
484 }
485
486 if coins_checked >= 1000 {
488 break;
489 }
490 }
491
492 Err(anyhow!(
493 "No suitable gas coin with >= {} mist found for address {:?} after checking {} coins",
494 minimal_amount,
495 address,
496 coins_checked
497 ))
498}
499
500#[derive(Debug, Eq, PartialEq, Clone)]
501pub struct EthContractAddresses {
502 pub sui_bridge: EthAddress,
503 pub bridge_committee: EthAddress,
504 pub bridge_config: EthAddress,
505 pub bridge_limiter: EthAddress,
506 pub bridge_vault: EthAddress,
507}