iota_bridge/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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    /// Rpc url for Eth fullnode, used for query stuff.
43    pub eth_rpc_url: String,
44    /// The proxy address of IotaBridge
45    pub eth_bridge_proxy_address: String,
46    /// The expected BridgeChainId on Eth side.
47    pub eth_bridge_chain_id: u8,
48    /// The starting block for EthSyncer to monitor eth contracts.
49    /// It is required when `run_client` is true. Usually this is
50    /// the block number when the bridge contracts are deployed.
51    /// When BridgeNode starts, it reads the contract watermark from storage.
52    /// If the watermark is not found, it will start from this fallback block
53    /// number. If the watermark is found, it will start from the watermark.
54    /// this v.s.`eth_contracts_start_block_override`:
55    pub eth_contracts_start_block_fallback: Option<u64>,
56    /// The starting block for EthSyncer to monitor eth contracts. It overrides
57    /// the watermark in storage. This is useful when we want to reprocess the
58    /// events from a specific block number.
59    /// Note: this field has to be reset after starting the BridgeNode,
60    /// otherwise it will reprocess the events from this block number every
61    /// time it starts.
62    #[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    /// Rpc url for IOTA fullnode, used for query stuff and submit transactions.
71    pub iota_rpc_url: String,
72    /// The expected BridgeChainId on IOTA side.
73    pub iota_bridge_chain_id: u8,
74    /// Path of the file where bridge client key (any IotaKeyPair) is stored.
75    /// If `run_client` is true, and this is None, then use
76    /// `bridge_authority_key_path` as client key.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub bridge_client_key_path: Option<PathBuf>,
79    /// The gas object to use for paying for gas fees for the client. It needs
80    /// to be owned by the address associated with bridge client key. If not
81    /// set and `run_client` is true, it will query and use the gas object
82    /// with highest amount for the account.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub bridge_client_gas_object: Option<ObjectID>,
85    /// Override the last processed EventID for bridge module `bridge`.
86    /// When set, IotaSyncer will start from this cursor (exclusively) instead
87    /// of the one in storage. If the cursor is not found in storage or
88    /// override, the query will start from genesis. Key: iota module,
89    /// Value: last processed EventID (tx_digest, event_seq). Note 1: This
90    /// field should be rarely used. Only use it when you understand how to
91    /// follow up. Note 2: the EventID needs to be valid, namely it must
92    /// exist and matches the filter. Otherwise, it will miss one event
93    /// because of fullnode Event query semantics.
94    #[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    /// The port that the server listens on.
103    pub server_listen_port: u16,
104    /// The port that for metrics server.
105    pub metrics_port: u16,
106    /// Path of the file where bridge authority key (Secp256k1) is stored.
107    pub bridge_authority_key_path: PathBuf,
108    /// Whether to run client. If true, `iota.bridge_client_key_path`
109    /// and `db_path` needs to be provided.
110    pub run_client: bool,
111    /// Path of the client storage. Required when `run_client` is true.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub db_path: Option<PathBuf>,
114    /// A list of approved governance actions. Action in this list will be
115    /// signed when requested by client.
116    pub approved_governance_actions: Vec<BridgeAction>,
117    /// IOTA configuration
118    pub iota: IotaConfig,
119    /// Eth configuration
120    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        // we do this check here instead of `prepare_for_iota` below because
147        // that is only called when `run_client` is true.
148        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        // Validate approved actions that must be governace actions
174        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        // If client is enabled, prepare client config
197        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            // in `prepare_for_eth` we check if this is None when `run_client` is true. Safe to
215            // unwrap here.
216            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        // If bridge chain id is Eth Mainent or Sepolia, we expect to see chain
251        // identifier to match accordingly.
252        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        // If bridge chain id is IOTA Mainent or Testnet, we expect to see chain
312        // identifier to match accordingly.
313        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        // TODO: decide a minimal amount here
343        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    /// A list of approved governance actions. Action in this list will be
384    /// signed when requested by client.
385    pub approved_governance_actions: Vec<BridgeAction>,
386}
387
388// TODO: add gas balance alert threshold
389pub 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    // See `BridgeNodeConfig` for the explanation of following two fields.
399    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}