1use std::{collections::HashSet, path::PathBuf, str::FromStr, sync::Arc};
6
7use anyhow::anyhow;
8use ethers::{providers::Middleware, types::Address as EthAddress};
9use futures::{StreamExt, future};
10use iota_config::Config;
11use iota_json_rpc_types::Coin;
12use iota_keys::keypair_file::read_key;
13use iota_sdk::{IotaClient as IotaSdkClient, IotaClientBuilder, apis::CoinReadApi};
14use iota_types::{
15 base_types::{IotaAddress, ObjectID, ObjectRef},
16 bridge::BridgeChainId,
17 crypto::{IotaKeyPair, KeypairTraits},
18 digests::{get_mainnet_chain_identifier, get_testnet_chain_identifier},
19 event::EventID,
20 object::Owner,
21};
22use serde::{Deserialize, Serialize};
23use serde_with::serde_as;
24use tracing::info;
25
26use crate::{
27 abi::EthBridgeConfig,
28 crypto::BridgeAuthorityKeyPair,
29 error::BridgeError,
30 eth_client::EthClient,
31 iota_client::IotaClient,
32 metered_eth_provider::{MeteredEthHttpProvider, new_metered_eth_provider},
33 metrics::BridgeMetrics,
34 types::{BridgeAction, is_route_valid},
35 utils::get_eth_contract_addresses,
36};
37
38#[serde_as]
39#[derive(Clone, Debug, Deserialize, Serialize)]
40#[serde(rename_all = "kebab-case")]
41pub struct EthConfig {
42 pub eth_rpc_url: String,
44 pub eth_bridge_proxy_address: String,
46 pub eth_bridge_chain_id: u8,
48 pub eth_contracts_start_block_fallback: Option<u64>,
56 #[serde(skip_serializing_if = "Option::is_none")]
63 pub eth_contracts_start_block_override: Option<u64>,
64}
65
66#[serde_as]
67#[derive(Clone, Debug, Deserialize, Serialize)]
68#[serde(rename_all = "kebab-case")]
69pub struct IotaConfig {
70 pub iota_rpc_url: String,
72 pub iota_bridge_chain_id: u8,
74 #[serde(skip_serializing_if = "Option::is_none")]
78 pub bridge_client_key_path: Option<PathBuf>,
79 #[serde(skip_serializing_if = "Option::is_none")]
84 pub bridge_client_gas_object: Option<ObjectID>,
85 #[serde(skip_serializing_if = "Option::is_none")]
95 pub iota_bridge_module_last_processed_event_id_override: Option<EventID>,
96}
97
98#[serde_as]
99#[derive(Clone, 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>,
117 pub iota: IotaConfig,
119 pub eth: EthConfig,
121}
122
123impl Config for BridgeNodeConfig {}
124
125impl BridgeNodeConfig {
126 pub async fn validate(
127 &self,
128 metrics: Arc<BridgeMetrics>,
129 ) -> anyhow::Result<(BridgeServerConfig, Option<BridgeClientConfig>)> {
130 if !is_route_valid(
131 BridgeChainId::try_from(self.iota.iota_bridge_chain_id)?,
132 BridgeChainId::try_from(self.eth.eth_bridge_chain_id)?,
133 ) {
134 return Err(anyhow!(
135 "Route between IOTA chain id {} and Eth chain id {} is not valid",
136 self.iota.iota_bridge_chain_id,
137 self.eth.eth_bridge_chain_id,
138 ));
139 };
140
141 let bridge_authority_key = match read_key(&self.bridge_authority_key_path, true)? {
142 IotaKeyPair::Secp256k1(key) => key,
143 _ => unreachable!("we required secp256k1 key in `read_key`"),
144 };
145
146 let iota_client =
149 Arc::new(IotaClient::<IotaSdkClient>::new(&self.iota.iota_rpc_url).await?);
150 let bridge_committee = iota_client
151 .get_bridge_committee()
152 .await
153 .map_err(|e| anyhow!("Error getting bridge committee: {:?}", e))?;
154 if !bridge_committee.is_active_member(&bridge_authority_key.public().into()) {
155 return Err(anyhow!(
156 "Bridge authority key is not part of bridge committee"
157 ));
158 }
159
160 let (eth_client, eth_contracts) = self.prepare_for_eth(metrics).await?;
161 let bridge_summary = iota_client
162 .get_bridge_summary()
163 .await
164 .map_err(|e| anyhow!("Error getting bridge summary: {:?}", e))?;
165 if bridge_summary.chain_id != self.iota.iota_bridge_chain_id {
166 anyhow::bail!(
167 "Bridge chain id mismatch: expected {}, but connected to {}",
168 self.iota.iota_bridge_chain_id,
169 bridge_summary.chain_id
170 );
171 }
172
173 for action in &self.approved_governance_actions {
175 if !action.is_governace_action() {
176 anyhow::bail!(format!(
177 "{:?}",
178 BridgeError::ActionIsNotGovernanceAction(action.clone())
179 ));
180 }
181 }
182 let approved_governance_actions = self.approved_governance_actions.clone();
183
184 let bridge_server_config = BridgeServerConfig {
185 key: bridge_authority_key,
186 metrics_port: self.metrics_port,
187 server_listen_port: self.server_listen_port,
188 iota_client: iota_client.clone(),
189 eth_client: eth_client.clone(),
190 approved_governance_actions,
191 };
192 if !self.run_client {
193 return Ok((bridge_server_config, None));
194 }
195
196 let (bridge_client_key, client_iota_address, gas_object_ref) =
198 self.prepare_for_iota(iota_client.clone()).await?;
199
200 let db_path = self
201 .db_path
202 .clone()
203 .ok_or(anyhow!("`db_path` is required when `run_client` is true"))?;
204
205 let bridge_client_config = BridgeClientConfig {
206 iota_address: client_iota_address,
207 key: bridge_client_key,
208 gas_object_ref,
209 metrics_port: self.metrics_port,
210 iota_client: iota_client.clone(),
211 eth_client: eth_client.clone(),
212 db_path,
213 eth_contracts,
214 eth_contracts_start_block_fallback: self
217 .eth
218 .eth_contracts_start_block_fallback
219 .unwrap(),
220 eth_contracts_start_block_override: self.eth.eth_contracts_start_block_override,
221 iota_bridge_module_last_processed_event_id_override: self
222 .iota
223 .iota_bridge_module_last_processed_event_id_override,
224 };
225
226 Ok((bridge_server_config, Some(bridge_client_config)))
227 }
228
229 async fn prepare_for_eth(
230 &self,
231 metrics: Arc<BridgeMetrics>,
232 ) -> anyhow::Result<(Arc<EthClient<MeteredEthHttpProvider>>, Vec<EthAddress>)> {
233 let bridge_proxy_address = EthAddress::from_str(&self.eth.eth_bridge_proxy_address)?;
234 let provider = Arc::new(
235 new_metered_eth_provider(&self.eth.eth_rpc_url, metrics.clone())
236 .unwrap()
237 .interval(std::time::Duration::from_millis(2000)),
238 );
239 let chain_id = provider.get_chainid().await?;
240 let (committee_address, limiter_address, vault_address, config_address) =
241 get_eth_contract_addresses(bridge_proxy_address, &provider).await?;
242 let config = EthBridgeConfig::new(config_address, provider.clone());
243
244 if self.run_client && self.eth.eth_contracts_start_block_fallback.is_none() {
245 return Err(anyhow!(
246 "eth_contracts_start_block_fallback is required when run_client is true"
247 ));
248 }
249
250 let bridge_chain_id: u8 = config.chain_id().call().await?;
253 if self.eth.eth_bridge_chain_id != bridge_chain_id {
254 return Err(anyhow!(
255 "Bridge chain id mismatch: expected {}, but connected to {}",
256 self.eth.eth_bridge_chain_id,
257 bridge_chain_id
258 ));
259 }
260 if bridge_chain_id == BridgeChainId::EthMainnet as u8 && chain_id.as_u64() != 1 {
261 anyhow::bail!(
262 "Expected Eth chain id 1, but connected to {}",
263 chain_id.as_u64()
264 );
265 }
266 if bridge_chain_id == BridgeChainId::EthSepolia as u8 && chain_id.as_u64() != 11155111 {
267 anyhow::bail!(
268 "Expected Eth chain id 11155111, but connected to {}",
269 chain_id.as_u64()
270 );
271 }
272 info!(
273 "Connected to Eth chain: {}, Bridge chain id: {}",
274 chain_id.as_u64(),
275 bridge_chain_id,
276 );
277
278 let eth_client = Arc::new(
279 EthClient::<MeteredEthHttpProvider>::new(
280 &self.eth.eth_rpc_url,
281 HashSet::from_iter(vec![
282 bridge_proxy_address,
283 committee_address,
284 config_address,
285 limiter_address,
286 vault_address,
287 ]),
288 metrics,
289 )
290 .await?,
291 );
292 let contract_addresses = vec![
293 bridge_proxy_address,
294 committee_address,
295 config_address,
296 limiter_address,
297 vault_address,
298 ];
299 Ok((eth_client, contract_addresses))
300 }
301
302 async fn prepare_for_iota(
303 &self,
304 iota_client: Arc<IotaClient<IotaSdkClient>>,
305 ) -> anyhow::Result<(IotaKeyPair, IotaAddress, ObjectRef)> {
306 let bridge_client_key = match &self.iota.bridge_client_key_path {
307 None => read_key(&self.bridge_authority_key_path, true),
308 Some(path) => read_key(path, false),
309 }?;
310
311 let iota_identifier = iota_client
314 .get_chain_identifier()
315 .await
316 .map_err(|e| anyhow!("Error getting chain identifier from IOTA: {:?}", e))?;
317 if self.iota.iota_bridge_chain_id == BridgeChainId::IotaMainnet as u8
318 && iota_identifier != get_mainnet_chain_identifier().to_string()
319 {
320 anyhow::bail!(
321 "Expected iota chain identifier {}, but connected to {}",
322 self.iota.iota_bridge_chain_id,
323 iota_identifier
324 );
325 }
326 if self.iota.iota_bridge_chain_id == BridgeChainId::IotaTestnet as u8
327 && iota_identifier != get_testnet_chain_identifier().to_string()
328 {
329 anyhow::bail!(
330 "Expected iota chain identifier {}, but connected to {}",
331 self.iota.iota_bridge_chain_id,
332 iota_identifier
333 );
334 }
335 info!(
336 "Connected to IOTA chain: {}, Bridge chain id: {}",
337 iota_identifier, self.iota.iota_bridge_chain_id,
338 );
339
340 let client_iota_address = IotaAddress::from(&bridge_client_key.public());
341
342 let gas_object_id = match self.iota.bridge_client_gas_object {
344 Some(id) => id,
345 None => {
346 let iota_client = IotaClientBuilder::default()
347 .build(&self.iota.iota_rpc_url)
348 .await?;
349 let coin =
350 pick_highest_balance_coin(iota_client.coin_read_api(), client_iota_address, 0)
351 .await?;
352 coin.coin_object_id
353 }
354 };
355 let (gas_coin, gas_object_ref, owner) = iota_client
356 .get_gas_data_panic_if_not_gas(gas_object_id)
357 .await;
358 if owner != Owner::AddressOwner(client_iota_address) {
359 return Err(anyhow!(
360 "Gas object {:?} is not owned by bridge client key's associated iota address {:?}, but {:?}",
361 gas_object_id,
362 client_iota_address,
363 owner
364 ));
365 }
366 info!(
367 "Starting bridge client with address: {:?}, gas object {:?}, balance: {}",
368 client_iota_address,
369 gas_object_ref.0,
370 gas_coin.value()
371 );
372
373 Ok((bridge_client_key, client_iota_address, gas_object_ref))
374 }
375}
376
377pub struct BridgeServerConfig {
378 pub key: BridgeAuthorityKeyPair,
379 pub server_listen_port: u16,
380 pub metrics_port: u16,
381 pub iota_client: Arc<IotaClient<IotaSdkClient>>,
382 pub eth_client: Arc<EthClient<MeteredEthHttpProvider>>,
383 pub approved_governance_actions: Vec<BridgeAction>,
386}
387
388pub struct BridgeClientConfig {
390 pub iota_address: IotaAddress,
391 pub key: IotaKeyPair,
392 pub gas_object_ref: ObjectRef,
393 pub metrics_port: u16,
394 pub iota_client: Arc<IotaClient<IotaSdkClient>>,
395 pub eth_client: Arc<EthClient<MeteredEthHttpProvider>>,
396 pub db_path: PathBuf,
397 pub eth_contracts: Vec<EthAddress>,
398 pub eth_contracts_start_block_fallback: u64,
400 pub eth_contracts_start_block_override: Option<u64>,
401 pub iota_bridge_module_last_processed_event_id_override: Option<EventID>,
402}
403
404#[serde_as]
405#[derive(Clone, Debug, Deserialize, Serialize)]
406#[serde(rename_all = "kebab-case")]
407pub struct BridgeCommitteeConfig {
408 pub bridge_authority_port_and_key_path: Vec<(u64, PathBuf)>,
409}
410
411impl Config for BridgeCommitteeConfig {}
412
413pub async fn pick_highest_balance_coin(
414 coin_read_api: &CoinReadApi,
415 address: IotaAddress,
416 minimal_amount: u64,
417) -> anyhow::Result<Coin> {
418 let mut highest_balance = 0;
419 let mut highest_balance_coin = None;
420 coin_read_api
421 .get_coins_stream(address, None)
422 .for_each(|coin: Coin| {
423 if coin.balance > highest_balance {
424 highest_balance = coin.balance;
425 highest_balance_coin = Some(coin.clone());
426 }
427 future::ready(())
428 })
429 .await;
430 if highest_balance_coin.is_none() {
431 return Err(anyhow!("No IOTA coins found for address {:?}", address));
432 }
433 if highest_balance < minimal_amount {
434 return Err(anyhow!(
435 "Found no single coin that has >= {} balance IOTA for address {:?}",
436 minimal_amount,
437 address,
438 ));
439 }
440 Ok(highest_balance_coin.unwrap())
441}
442
443#[derive(Debug, Eq, PartialEq, Clone)]
444pub struct EthContractAddresses {
445 pub iota_bridge: EthAddress,
446 pub bridge_committee: EthAddress,
447 pub bridge_config: EthAddress,
448 pub bridge_limiter: EthAddress,
449 pub bridge_vault: EthAddress,
450}