sui_bridge/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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_json_rpc_types::Coin;
26use sui_keys::keypair_file::read_key;
27use sui_sdk::SuiClientBuilder;
28use sui_sdk::apis::CoinReadApi;
29use sui_types::base_types::ObjectRef;
30use sui_types::base_types::{ObjectID, SuiAddress};
31use sui_types::bridge::BridgeChainId;
32use sui_types::crypto::KeypairTraits;
33use sui_types::crypto::{NetworkKeyPair, SuiKeyPair, get_key_pair_from_rng};
34use sui_types::digests::{get_mainnet_chain_identifier, get_testnet_chain_identifier};
35use sui_types::event::EventID;
36use sui_types::object::Owner;
37use tracing::info;
38
39#[serde_as]
40#[derive(Clone, Debug, Deserialize, Serialize)]
41#[serde(rename_all = "kebab-case")]
42pub struct EthConfig {
43    /// Rpc url for Eth fullnode, used for query stuff.
44    pub eth_rpc_url: String,
45    /// The proxy address of SuiBridge
46    pub eth_bridge_proxy_address: String,
47    /// The expected BridgeChainId on Eth side.
48    pub eth_bridge_chain_id: u8,
49    /// The starting block for EthSyncer to monitor eth contracts.
50    /// It is required when `run_client` is true. Usually this is
51    /// the block number when the bridge contracts are deployed.
52    /// When BridgeNode starts, it reads the contract watermark from storage.
53    /// If the watermark is not found, it will start from this fallback block number.
54    /// If the watermark is found, it will start from the watermark.
55    /// this v.s.`eth_contracts_start_block_override`:
56    pub eth_contracts_start_block_fallback: Option<u64>,
57    /// The starting block for EthSyncer to monitor eth contracts. It overrides
58    /// the watermark in storage. This is useful when we want to reprocess the events
59    /// from a specific block number.
60    /// Note: this field has to be reset after starting the BridgeNode, otherwise it will
61    /// reprocess the events from this block number every 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 SuiConfig {
70    /// Rpc url for Sui fullnode, used for query stuff and submit transactions.
71    pub sui_rpc_url: String,
72    /// The expected BridgeChainId on Sui side.
73    pub sui_bridge_chain_id: u8,
74    /// Path of the file where bridge client key (any SuiKeyPair) is stored.
75    /// If `run_client` is true, and this is None, then use `bridge_authority_key_path` as client key.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub bridge_client_key_path: Option<PathBuf>,
78    /// The gas object to use for paying for gas fees for the client. It needs to
79    /// be owned by the address associated with bridge client key. If not set
80    /// and `run_client` is true, it will query and use the gas object with highest
81    /// amount for the account.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub bridge_client_gas_object: Option<ObjectID>,
84    /// Override the last processed EventID for bridge module `bridge`.
85    /// When set, SuiSyncer will start from this cursor (exclusively) instead of the one in storage.
86    /// If the cursor is not found in storage or override, the query will start from genesis.
87    /// Key: sui module, Value: last processed EventID (tx_digest, event_seq).
88    /// Note 1: This field should be rarely used. Only use it when you understand how to follow up.
89    /// Note 2: the EventID needs to be valid, namely it must exist and matches the filter.
90    /// Otherwise, it will miss one event because of fullnode Event query semantics.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub sui_bridge_module_last_processed_event_id_override: Option<EventID>,
93    /// Override the next sequence number for SuiSyncer
94    /// When set, SuiSyncer will start from this sequence number (exclusively) instead of the one in storage.
95    /// If the sequence number is not found in storage or override, the query will first fallback to the sequence number corresponding to the last processed EventID from the bridge module `bridge` (which in turn can be overridden via `sui_bridge_module_last_processed_event_id_override`) if available, otherwise fallback to 0.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub sui_bridge_next_sequence_number_override: Option<u64>,
98}
99
100#[serde_as]
101#[derive(Debug, Deserialize, Serialize)]
102#[serde(rename_all = "kebab-case")]
103pub struct BridgeNodeConfig {
104    /// The port that the server listens on.
105    pub server_listen_port: u16,
106    /// The port that for metrics server.
107    pub metrics_port: u16,
108    /// Path of the file where bridge authority key (Secp256k1) is stored.
109    pub bridge_authority_key_path: PathBuf,
110    /// Whether to run client. If true, `sui.bridge_client_key_path`
111    /// and `db_path` needs to be provided.
112    pub run_client: bool,
113    /// Path of the client storage. Required when `run_client` is true.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub db_path: Option<PathBuf>,
116    /// A list of approved governance actions. Action in this list will be signed when requested by client.
117    pub approved_governance_actions: Vec<BridgeAction>,
118    /// Sui configuration
119    pub sui: SuiConfig,
120    /// Eth configuration
121    pub eth: EthConfig,
122    /// Network key used for metrics pushing
123    #[serde(default = "default_ed25519_key_pair")]
124    pub metrics_key_pair: NetworkKeyPair,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub metrics: Option<MetricsConfig>,
127
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub watchdog_config: Option<WatchdogConfig>,
130}
131
132pub fn default_ed25519_key_pair() -> NetworkKeyPair {
133    get_key_pair_from_rng(&mut rand::rngs::OsRng).1
134}
135
136#[derive(Debug, Clone, Deserialize, Serialize)]
137#[serde(rename_all = "kebab-case")]
138pub struct MetricsConfig {
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub push_interval_seconds: Option<u64>,
141    pub push_url: String,
142}
143
144#[derive(Debug, Clone, Deserialize, Serialize)]
145#[serde(rename_all = "kebab-case")]
146pub struct WatchdogConfig {
147    /// Total supplies to watch on Sui. Mapping from coin name to coin type tag
148    pub total_supplies: BTreeMap<String, String>,
149}
150
151impl Config for BridgeNodeConfig {}
152
153impl BridgeNodeConfig {
154    pub async fn validate(
155        &self,
156        metrics: Arc<BridgeMetrics>,
157    ) -> anyhow::Result<(BridgeServerConfig, Option<BridgeClientConfig>)> {
158        info!("Starting config validation");
159        if !is_route_valid(
160            BridgeChainId::try_from(self.sui.sui_bridge_chain_id)?,
161            BridgeChainId::try_from(self.eth.eth_bridge_chain_id)?,
162        ) {
163            return Err(anyhow!(
164                "Route between Sui chain id {} and Eth chain id {} is not valid",
165                self.sui.sui_bridge_chain_id,
166                self.eth.eth_bridge_chain_id,
167            ));
168        };
169
170        let bridge_authority_key = match read_key(&self.bridge_authority_key_path, true)? {
171            SuiKeyPair::Secp256k1(key) => key,
172            _ => unreachable!("we required secp256k1 key in `read_key`"),
173        };
174
175        // we do this check here instead of `prepare_for_sui` below because
176        // that is only called when `run_client` is true.
177        let sui_client =
178            Arc::new(SuiBridgeClient::new(&self.sui.sui_rpc_url, metrics.clone()).await?);
179        let bridge_committee = sui_client
180            .get_bridge_committee()
181            .await
182            .map_err(|e| anyhow!("Error getting bridge committee: {:?}", e))?;
183        if !bridge_committee.is_active_member(&bridge_authority_key.public().into()) {
184            return Err(anyhow!(
185                "Bridge authority key is not part of bridge committee"
186            ));
187        }
188
189        let (eth_client, eth_contracts) = self.prepare_for_eth(metrics.clone()).await?;
190        let bridge_summary = sui_client
191            .get_bridge_summary()
192            .await
193            .map_err(|e| anyhow!("Error getting bridge summary: {:?}", e))?;
194        if bridge_summary.chain_id != self.sui.sui_bridge_chain_id {
195            anyhow::bail!(
196                "Bridge chain id mismatch: expected {}, but connected to {}",
197                self.sui.sui_bridge_chain_id,
198                bridge_summary.chain_id
199            );
200        }
201
202        // Validate approved actions that must be governance actions
203        for action in &self.approved_governance_actions {
204            if !action.is_governance_action() {
205                anyhow::bail!(format!(
206                    "{:?}",
207                    BridgeError::ActionIsNotGovernanceAction(Box::new(action.clone()))
208                ));
209            }
210        }
211        let approved_governance_actions = self.approved_governance_actions.clone();
212
213        let bridge_server_config = BridgeServerConfig {
214            key: bridge_authority_key,
215            metrics_port: self.metrics_port,
216            eth_bridge_proxy_address: eth_contracts[0], // the first contract is bridge proxy
217            server_listen_port: self.server_listen_port,
218            sui_client: sui_client.clone(),
219            eth_client: eth_client.clone(),
220            approved_governance_actions,
221        };
222        if !self.run_client {
223            return Ok((bridge_server_config, None));
224        }
225
226        // If client is enabled, prepare client config
227        let (bridge_client_key, client_sui_address, gas_object_ref) =
228            self.prepare_for_sui(sui_client.clone(), metrics).await?;
229
230        let db_path = self
231            .db_path
232            .clone()
233            .ok_or(anyhow!("`db_path` is required when `run_client` is true"))?;
234
235        let bridge_client_config = BridgeClientConfig {
236            sui_address: client_sui_address,
237            key: bridge_client_key,
238            gas_object_ref,
239            metrics_port: self.metrics_port,
240            sui_client: sui_client.clone(),
241            eth_client: eth_client.clone(),
242            db_path,
243            eth_contracts,
244            // in `prepare_for_eth` we check if this is None when `run_client` is true. Safe to unwrap here.
245            eth_contracts_start_block_fallback: self
246                .eth
247                .eth_contracts_start_block_fallback
248                .unwrap(),
249            eth_contracts_start_block_override: self.eth.eth_contracts_start_block_override,
250            sui_bridge_module_last_processed_event_id_override: self
251                .sui
252                .sui_bridge_module_last_processed_event_id_override,
253            sui_bridge_next_sequence_number_override: self
254                .sui
255                .sui_bridge_next_sequence_number_override,
256            sui_bridge_chain_id: self.sui.sui_bridge_chain_id,
257        };
258
259        info!("Config validation complete");
260        Ok((bridge_server_config, Some(bridge_client_config)))
261    }
262
263    async fn prepare_for_eth(
264        &self,
265        metrics: Arc<BridgeMetrics>,
266    ) -> anyhow::Result<(Arc<EthClient>, Vec<EthAddress>)> {
267        info!("Creating Ethereum client provider");
268        let bridge_proxy_address = EthAddress::from_str(&self.eth.eth_bridge_proxy_address)?;
269        let provider = new_metered_eth_provider(&self.eth.eth_rpc_url, metrics.clone()).unwrap();
270        let chain_id = provider.get_chain_id().await?;
271        let (
272            committee_address,
273            limiter_address,
274            vault_address,
275            config_address,
276            _weth_address,
277            _usdt_address,
278            _wbtc_address,
279            _lbtc_address,
280        ) = get_eth_contract_addresses(bridge_proxy_address, provider.clone()).await?;
281        let config = EthBridgeConfig::new(config_address, provider.clone());
282
283        if self.run_client && self.eth.eth_contracts_start_block_fallback.is_none() {
284            return Err(anyhow!(
285                "eth_contracts_start_block_fallback is required when run_client is true"
286            ));
287        }
288
289        // If bridge chain id is Eth Mainent or Sepolia, we expect to see chain
290        // identifier to match accordingly.
291        let bridge_chain_id: u8 = config.chainID().call().await?;
292        if self.eth.eth_bridge_chain_id != bridge_chain_id {
293            return Err(anyhow!(
294                "Bridge chain id mismatch: expected {}, but connected to {}",
295                self.eth.eth_bridge_chain_id,
296                bridge_chain_id
297            ));
298        }
299        if bridge_chain_id == BridgeChainId::EthMainnet as u8 && chain_id != 1 {
300            anyhow::bail!("Expected Eth chain id 1, but connected to {}", chain_id);
301        }
302        if bridge_chain_id == BridgeChainId::EthSepolia as u8 && chain_id != 11155111 {
303            anyhow::bail!(
304                "Expected Eth chain id 11155111, but connected to {}",
305                chain_id
306            );
307        }
308        info!(
309            "Connected to Eth chain: {}, Bridge chain id: {}",
310            chain_id, bridge_chain_id,
311        );
312
313        let eth_client = Arc::new(
314            EthClient::new(
315                &self.eth.eth_rpc_url,
316                HashSet::from_iter(vec![
317                    bridge_proxy_address,
318                    committee_address,
319                    config_address,
320                    limiter_address,
321                    vault_address,
322                ]),
323                metrics,
324            )
325            .await?,
326        );
327        let contract_addresses = vec![
328            bridge_proxy_address,
329            committee_address,
330            config_address,
331            limiter_address,
332            vault_address,
333        ];
334        info!("Ethereum client setup complete");
335        Ok((eth_client, contract_addresses))
336    }
337
338    async fn prepare_for_sui(
339        &self,
340        sui_client: Arc<SuiBridgeClient>,
341        metrics: Arc<BridgeMetrics>,
342    ) -> anyhow::Result<(SuiKeyPair, SuiAddress, ObjectRef)> {
343        let bridge_client_key = match &self.sui.bridge_client_key_path {
344            None => read_key(&self.bridge_authority_key_path, true),
345            Some(path) => read_key(path, false),
346        }?;
347
348        // If bridge chain id is Sui Mainent or Testnet, we expect to see chain
349        // identifier to match accordingly.
350        let sui_identifier = sui_client
351            .get_chain_identifier()
352            .await
353            .map_err(|e| anyhow!("Error getting chain identifier from Sui: {:?}", e))?;
354        if self.sui.sui_bridge_chain_id == BridgeChainId::SuiMainnet as u8
355            && sui_identifier != get_mainnet_chain_identifier().to_string()
356        {
357            anyhow::bail!(
358                "Expected sui chain identifier {}, but connected to {}",
359                self.sui.sui_bridge_chain_id,
360                sui_identifier
361            );
362        }
363        if self.sui.sui_bridge_chain_id == BridgeChainId::SuiTestnet as u8
364            && sui_identifier != get_testnet_chain_identifier().to_string()
365        {
366            anyhow::bail!(
367                "Expected sui chain identifier {}, but connected to {}",
368                self.sui.sui_bridge_chain_id,
369                sui_identifier
370            );
371        }
372        info!(
373            "Connected to Sui chain: {}, Bridge chain id: {}",
374            sui_identifier, self.sui.sui_bridge_chain_id,
375        );
376
377        let client_sui_address = SuiAddress::from(&bridge_client_key.public());
378
379        let gas_object_id = match self.sui.bridge_client_gas_object {
380            Some(id) => id,
381            None => {
382                info!("No gas object configured, finding gas object with highest balance");
383                let sui_client = SuiClientBuilder::default()
384                    .build(&self.sui.sui_rpc_url)
385                    .await?;
386                let coin =
387                    // Minimum balance for gas object is 10 SUI
388                    pick_highest_balance_coin(sui_client.coin_read_api(), client_sui_address, 10_000_000_000)
389                        .await?;
390                coin.coin_object_id
391            }
392        };
393        let (gas_coin, gas_object_ref, owner) = sui_client
394            .get_gas_data_panic_if_not_gas(gas_object_id)
395            .await;
396        if owner != Owner::AddressOwner(client_sui_address) {
397            return Err(anyhow!(
398                "Gas object {:?} is not owned by bridge client key's associated sui address {:?}, but {:?}",
399                gas_object_id,
400                client_sui_address,
401                owner
402            ));
403        }
404        let balance = gas_coin.value();
405        info!("Gas object balance: {}", balance);
406        metrics.gas_coin_balance.set(balance as i64);
407
408        info!("Sui client setup complete");
409        Ok((bridge_client_key, client_sui_address, gas_object_ref))
410    }
411}
412
413pub struct BridgeServerConfig {
414    pub key: BridgeAuthorityKeyPair,
415    pub server_listen_port: u16,
416    pub eth_bridge_proxy_address: EthAddress,
417    pub metrics_port: u16,
418    pub sui_client: Arc<SuiBridgeClient>,
419    pub eth_client: Arc<EthClient>,
420    /// A list of approved governance actions. Action in this list will be signed when requested by client.
421    pub approved_governance_actions: Vec<BridgeAction>,
422}
423
424pub struct BridgeClientConfig {
425    pub sui_address: SuiAddress,
426    pub key: SuiKeyPair,
427    pub gas_object_ref: ObjectRef,
428    pub metrics_port: u16,
429    pub sui_client: Arc<SuiBridgeClient>,
430    pub eth_client: Arc<EthClient>,
431    pub db_path: PathBuf,
432    pub eth_contracts: Vec<EthAddress>,
433    // See `BridgeNodeConfig` for the explanation of following two fields.
434    pub eth_contracts_start_block_fallback: u64,
435    pub eth_contracts_start_block_override: Option<u64>,
436    pub sui_bridge_module_last_processed_event_id_override: Option<EventID>,
437    pub sui_bridge_next_sequence_number_override: Option<u64>,
438    pub sui_bridge_chain_id: u8,
439}
440
441#[serde_as]
442#[derive(Clone, Debug, Deserialize, Serialize)]
443#[serde(rename_all = "kebab-case")]
444pub struct BridgeCommitteeConfig {
445    pub bridge_authority_port_and_key_path: Vec<(u64, PathBuf)>,
446}
447
448impl Config for BridgeCommitteeConfig {}
449
450pub async fn pick_highest_balance_coin(
451    coin_read_api: &CoinReadApi,
452    address: SuiAddress,
453    minimal_amount: u64,
454) -> anyhow::Result<Coin> {
455    info!("Looking for a suitable gas coin for address {:?}", address);
456
457    // Only look at SUI coins specifically
458    let mut stream = coin_read_api
459        .get_coins_stream(address, Some("0x2::sui::SUI".to_string()))
460        .boxed();
461
462    let mut coins_checked = 0;
463
464    while let Some(coin) = stream.next().await {
465        info!(
466            "Checking coin: {:?}, balance: {}",
467            coin.coin_object_id, coin.balance
468        );
469        coins_checked += 1;
470
471        // Take the first coin with a sufficient balance
472        if coin.balance >= minimal_amount {
473            info!(
474                "Found suitable gas coin with {} mist (object ID: {:?})",
475                coin.balance, coin.coin_object_id
476            );
477            return Ok(coin);
478        }
479
480        // Only check a small number of coins before giving up
481        if coins_checked >= 1000 {
482            break;
483        }
484    }
485
486    Err(anyhow!(
487        "No suitable gas coin with >= {} mist found for address {:?} after checking {} coins",
488        minimal_amount,
489        address,
490        coins_checked
491    ))
492}
493
494#[derive(Debug, Eq, PartialEq, Clone)]
495pub struct EthContractAddresses {
496    pub sui_bridge: EthAddress,
497    pub bridge_committee: EthAddress,
498    pub bridge_config: EthAddress,
499    pub bridge_limiter: EthAddress,
500    pub bridge_vault: EthAddress,
501}