diff --git a/crates/sui-bridge/src/client/bridge_authority_aggregator.rs b/crates/sui-bridge/src/client/bridge_authority_aggregator.rs index e4e7947622198..882e689f341f2 100644 --- a/crates/sui-bridge/src/client/bridge_authority_aggregator.rs +++ b/crates/sui-bridge/src/client/bridge_authority_aggregator.rs @@ -31,7 +31,7 @@ pub struct BridgeAuthorityAggregator { impl BridgeAuthorityAggregator { pub fn new(committee: Arc) -> Self { - let clients = committee + let clients: BTreeMap> = committee .members() .iter() .filter_map(|(name, authority)| { diff --git a/crates/sui-bridge/src/e2e_tests/basic.rs b/crates/sui-bridge/src/e2e_tests/basic.rs index b56e322cd585e..e1d773ac81187 100644 --- a/crates/sui-bridge/src/e2e_tests/basic.rs +++ b/crates/sui-bridge/src/e2e_tests/basic.rs @@ -85,6 +85,7 @@ async fn test_bridge_from_eth_to_sui_to_eth() { .expect("Recipient should have received ETH coin now") .clone(); assert_eq!(eth_coin.balance, sui_amount); + info!("Eth to sui bridge transfer finished"); // Now let the recipient send the coin back to ETH let eth_address_1 = EthAddress::random(); diff --git a/crates/sui-bridge/src/lib.rs b/crates/sui-bridge/src/lib.rs index 0d24936eda2c4..71013b2c8d046 100644 --- a/crates/sui-bridge/src/lib.rs +++ b/crates/sui-bridge/src/lib.rs @@ -13,6 +13,7 @@ pub mod eth_syncer; pub mod eth_transaction_builder; pub mod events; pub mod metrics; +pub mod monitor; pub mod node; pub mod orchestrator; pub mod server; diff --git a/crates/sui-bridge/src/monitor.rs b/crates/sui-bridge/src/monitor.rs new file mode 100644 index 0000000000000..4ee9dde82c687 --- /dev/null +++ b/crates/sui-bridge/src/monitor.rs @@ -0,0 +1,368 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! `BridgeMonitor` receives all `SuiBridgeEvent` and handles them accordingly. + +use arc_swap::ArcSwap; +use std::sync::Arc; +use tokio::time::Duration; + +use crate::client::bridge_authority_aggregator::BridgeAuthorityAggregator; +use crate::crypto::BridgeAuthorityPublicKeyBytes; +use crate::events::CommitteeMemberUrlUpdateEvent; +use crate::events::SuiBridgeEvent; +use crate::retry_with_max_elapsed_time; +use crate::sui_client::{SuiClient, SuiClientInner}; +use crate::types::BridgeCommittee; +use tracing::{error, info, warn}; + +const REFRESH_COMMITTEE_RETRY_TIMES: u64 = 3; + +pub struct BridgeMonitor { + sui_client: Arc>, + monitor_rx: mysten_metrics::metered_channel::Receiver, + bridge_auth_agg: Arc>, +} + +impl BridgeMonitor +where + C: SuiClientInner + 'static, +{ + pub fn new( + sui_client: Arc>, + monitor_rx: mysten_metrics::metered_channel::Receiver, + bridge_auth_agg: Arc>, + ) -> Self { + Self { + sui_client, + monitor_rx, + bridge_auth_agg, + } + } + + pub async fn run(self) { + tracing::info!("Starting BridgeMonitor"); + let Self { + sui_client, + mut monitor_rx, + bridge_auth_agg, + } = self; + + while let Some(events) = monitor_rx.recv().await { + match events { + SuiBridgeEvent::SuiToEthTokenBridgeV1(_) => (), + SuiBridgeEvent::TokenTransferApproved(_) => (), + SuiBridgeEvent::TokenTransferClaimed(_) => (), + SuiBridgeEvent::TokenTransferAlreadyApproved(_) => (), + SuiBridgeEvent::TokenTransferAlreadyClaimed(_) => (), + SuiBridgeEvent::TokenTransferLimitExceed(_) => { + // TODO + } + SuiBridgeEvent::EmergencyOpEvent(_) => { + // TODO + } + SuiBridgeEvent::CommitteeMemberRegistration(_) => (), + SuiBridgeEvent::CommitteeUpdateEvent(_) => (), + SuiBridgeEvent::CommitteeMemberUrlUpdateEvent(event) => { + info!("Received CommitteeMemberUrlUpdateEvent: {:?}", event); + let new_committee = get_latest_bridge_committee_with_url_update_event( + sui_client.clone(), + event, + Duration::from_secs(10), + ) + .await; + bridge_auth_agg.store(Arc::new(BridgeAuthorityAggregator::new(Arc::new( + new_committee, + )))); + info!("Committee updated"); + } + SuiBridgeEvent::BlocklistValidatorEvent(_) => { + // TODO + } + SuiBridgeEvent::TokenRegistrationEvent(_) => (), + SuiBridgeEvent::NewTokenEvent(_) => { + // TODO + } + SuiBridgeEvent::UpdateTokenPriceEvent(_) => (), + } + } + + panic!("BridgeMonitor channel was closed unexpectedly"); + } +} + +async fn get_latest_bridge_committee_with_url_update_event( + sui_client: Arc>, + event: CommitteeMemberUrlUpdateEvent, + staleness_retry_interval: Duration, +) -> BridgeCommittee { + let mut remaining_retry_times = REFRESH_COMMITTEE_RETRY_TIMES; + loop { + let Ok(Ok(committee)) = retry_with_max_elapsed_time!( + sui_client.get_bridge_committee(), + Duration::from_secs(600) + ) else { + error!("Failed to get bridge committee after retry"); + continue; + }; + let member = committee.member(&BridgeAuthorityPublicKeyBytes::from(&event.member)); + let Some(member) = member else { + // This is possible when a node is processing an older event while the member quitted at a later point, which is fine. + // Or fullnode returns a stale committee that the member hasn't joined, which is rare and tricy to handle so we just log it. + warn!( + "Committee member not found in the committee: {:?}", + event.member + ); + return committee; + }; + if member.base_url == event.new_url { + return committee; + } + // If url does not match, it could be: + // 1. the query is sent to a stale fullnode that does not have the latest data yet + // 2. the node is processing an older message, and the latest url has changed again + // In either case, we retry a few times. If it still fails to match, we assume it's the latter case. + tokio::time::sleep(staleness_retry_interval).await; + remaining_retry_times -= 1; + if remaining_retry_times == 0 { + warn!( + "Committee member url {:?} does not match onchain record {:?} after retry", + event.member, member + ); + return committee; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::events::init_all_struct_tags; + use crate::test_utils::{ + bridge_committee_to_bridge_committee_summary, get_test_authority_and_key, + }; + use fastcrypto::traits::KeyPair; + use prometheus::Registry; + use sui_types::base_types::SuiAddress; + use sui_types::bridge::BridgeCommitteeSummary; + use sui_types::bridge::MoveTypeCommitteeMember; + use sui_types::crypto::get_key_pair; + + use crate::{sui_mock_client::SuiMockClient, types::BridgeCommittee}; + use sui_types::crypto::ToFromBytes; + + #[tokio::test] + async fn test_get_latest_bridge_committee_with_url_update_event() { + telemetry_subscribers::init_for_testing(); + let sui_client_mock = SuiMockClient::default(); + let sui_client = Arc::new(SuiClient::new_for_testing(sui_client_mock.clone())); + let (_, kp): (_, fastcrypto::secp256k1::Secp256k1KeyPair) = get_key_pair(); + let pk = kp.public().clone(); + let pk_as_bytes = BridgeAuthorityPublicKeyBytes::from(&pk); + let pk_bytes = pk_as_bytes.as_bytes().to_vec(); + let event = CommitteeMemberUrlUpdateEvent { + member: pk, + new_url: "http://new.url".to_string(), + }; + let summary = BridgeCommitteeSummary { + members: vec![( + pk_bytes.clone(), + MoveTypeCommitteeMember { + sui_address: SuiAddress::random_for_testing_only(), + bridge_pubkey_bytes: pk_bytes.clone(), + voting_power: 10000, + http_rest_url: "http://new.url".to_string().as_bytes().to_vec(), + blocklisted: false, + }, + )], + member_registration: vec![], + last_committee_update_epoch: 0, + }; + + // Test the regular case, the onchain url matches + sui_client_mock.set_bridge_committee(summary.clone()); + let timer = std::time::Instant::now(); + let committee = get_latest_bridge_committee_with_url_update_event( + sui_client.clone(), + event.clone(), + Duration::from_secs(2), + ) + .await; + assert_eq!( + committee.member(&pk_as_bytes).unwrap().base_url, + "http://new.url" + ); + assert!(timer.elapsed().as_millis() < 500); + + // Test the case where the onchain url is older. Then update onchain url in 1 second. + // Since the retry interval is 2 seconds, it should return the next retry. + let old_summary = BridgeCommitteeSummary { + members: vec![( + pk_bytes.clone(), + MoveTypeCommitteeMember { + sui_address: SuiAddress::random_for_testing_only(), + bridge_pubkey_bytes: pk_bytes.clone(), + voting_power: 10000, + http_rest_url: "http://old.url".to_string().as_bytes().to_vec(), + blocklisted: false, + }, + )], + member_registration: vec![], + last_committee_update_epoch: 0, + }; + sui_client_mock.set_bridge_committee(old_summary.clone()); + let timer = std::time::Instant::now(); + // update the url to "http://new.url" in 1 second + let sui_client_mock_clone = sui_client_mock.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + sui_client_mock_clone.set_bridge_committee(summary.clone()); + }); + let committee = get_latest_bridge_committee_with_url_update_event( + sui_client.clone(), + event.clone(), + Duration::from_secs(2), + ) + .await; + assert_eq!( + committee.member(&pk_as_bytes).unwrap().base_url, + "http://new.url" + ); + let elapsed = timer.elapsed().as_millis(); + assert!(elapsed > 1000 && elapsed < 3000); + + // Test the case where the onchain url is newer. It should retry up to + // REFRESH_COMMITTEE_RETRY_TIMES time then return the onchain record. + let newer_summary = BridgeCommitteeSummary { + members: vec![( + pk_bytes.clone(), + MoveTypeCommitteeMember { + sui_address: SuiAddress::random_for_testing_only(), + bridge_pubkey_bytes: pk_bytes.clone(), + voting_power: 10000, + http_rest_url: "http://newer.url".to_string().as_bytes().to_vec(), + blocklisted: false, + }, + )], + member_registration: vec![], + last_committee_update_epoch: 0, + }; + sui_client_mock.set_bridge_committee(newer_summary.clone()); + let timer = std::time::Instant::now(); + let committee = get_latest_bridge_committee_with_url_update_event( + sui_client.clone(), + event.clone(), + Duration::from_millis(500), + ) + .await; + assert_eq!( + committee.member(&pk_as_bytes).unwrap().base_url, + "http://newer.url" + ); + let elapsed = timer.elapsed().as_millis(); + assert!(elapsed > 500 * REFRESH_COMMITTEE_RETRY_TIMES as u128); + + // Test the case where the member is not found in the committee + // It should return the onchain record. + let (_, kp2): (_, fastcrypto::secp256k1::Secp256k1KeyPair) = get_key_pair(); + let pk2 = kp2.public().clone(); + let pk_as_bytes2 = BridgeAuthorityPublicKeyBytes::from(&pk2); + let pk_bytes2 = pk_as_bytes2.as_bytes().to_vec(); + let newer_summary = BridgeCommitteeSummary { + members: vec![( + pk_bytes2.clone(), + MoveTypeCommitteeMember { + sui_address: SuiAddress::random_for_testing_only(), + bridge_pubkey_bytes: pk_bytes2.clone(), + voting_power: 10000, + http_rest_url: "http://newer.url".to_string().as_bytes().to_vec(), + blocklisted: false, + }, + )], + member_registration: vec![], + last_committee_update_epoch: 0, + }; + sui_client_mock.set_bridge_committee(newer_summary.clone()); + let timer = std::time::Instant::now(); + let committee = get_latest_bridge_committee_with_url_update_event( + sui_client.clone(), + event.clone(), + Duration::from_secs(1), + ) + .await; + assert_eq!( + committee.member(&pk_as_bytes2).unwrap().base_url, + "http://newer.url" + ); + assert!(committee.member(&pk_as_bytes).is_none()); + let elapsed = timer.elapsed().as_millis(); + assert!(elapsed < 1000); + } + + #[tokio::test] + async fn test_update_bridge_authority_aggregation_with_url_change_event() { + let (monitor_tx, monitor_rx, sui_client_mock, sui_client) = setup(); + let mut authorities = vec![ + get_test_authority_and_key(2500, 0 /* port, dummy value */).0, + get_test_authority_and_key(2500, 0 /* port, dummy value */).0, + get_test_authority_and_key(2500, 0 /* port, dummy value */).0, + get_test_authority_and_key(2500, 0 /* port, dummy value */).0, + ]; + let old_committee = BridgeCommittee::new(authorities.clone()).unwrap(); + let agg = Arc::new(ArcSwap::new(Arc::new(BridgeAuthorityAggregator::new( + Arc::new(old_committee), + )))); + let _handle = tokio::task::spawn( + BridgeMonitor::new(sui_client.clone(), monitor_rx, agg.clone()).run(), + ); + let new_url = "http://new.url".to_string(); + authorities[0].base_url = new_url.clone(); + let new_committee = BridgeCommittee::new(authorities.clone()).unwrap(); + let new_committee_summary = + bridge_committee_to_bridge_committee_summary(new_committee.clone()); + sui_client_mock.set_bridge_committee(new_committee_summary.clone()); + monitor_tx + .send(SuiBridgeEvent::CommitteeMemberUrlUpdateEvent( + CommitteeMemberUrlUpdateEvent { + member: authorities[0].pubkey.clone(), + new_url: new_url.clone(), + }, + )) + .await + .unwrap(); + // Wait for the monitor to process the event + tokio::time::sleep(Duration::from_secs(1)).await; + // Now expect the committee to be updated + assert_eq!( + agg.load() + .committee + .member(&BridgeAuthorityPublicKeyBytes::from(&authorities[0].pubkey)) + .unwrap() + .base_url, + new_url + ); + } + + fn setup() -> ( + mysten_metrics::metered_channel::Sender, + mysten_metrics::metered_channel::Receiver, + SuiMockClient, + Arc>, + ) { + telemetry_subscribers::init_for_testing(); + let registry = Registry::new(); + mysten_metrics::init_metrics(®istry); + init_all_struct_tags(); + + let sui_client_mock = SuiMockClient::default(); + let sui_client = Arc::new(SuiClient::new_for_testing(sui_client_mock.clone())); + let (monitor_tx, monitor_rx) = mysten_metrics::metered_channel::channel( + 10000, + &mysten_metrics::get_metrics() + .unwrap() + .channel_inflight + .with_label_values(&["monitor_queue"]), + ); + (monitor_tx, monitor_rx, sui_client_mock, sui_client) + } +} diff --git a/crates/sui-bridge/src/node.rs b/crates/sui-bridge/src/node.rs index 6614941e9b0fc..b9442efb4e9a6 100644 --- a/crates/sui-bridge/src/node.rs +++ b/crates/sui-bridge/src/node.rs @@ -8,6 +8,7 @@ use crate::{ eth_syncer::EthSyncer, events::init_all_struct_tags, metrics::BridgeMetrics, + monitor::BridgeMonitor, orchestrator::BridgeOrchestrator, server::{handler::BridgeRequestHandler, run_server, BridgeNodePublicMetadata}, storage::BridgeOrchestratorTables, @@ -15,6 +16,7 @@ use crate::{ }; use arc_swap::ArcSwap; use ethers::types::Address as EthAddress; +use mysten_metrics::spawn_logged_monitored_task; use std::{ collections::HashMap, net::{IpAddr, Ipv4Addr, SocketAddr}, @@ -111,9 +113,17 @@ async fn start_client_components( let sui_token_type_tags = sui_client.get_token_id_map().await.unwrap(); let (token_type_tags_tx, token_type_tags_rx) = tokio::sync::watch::channel(sui_token_type_tags); + let (monitor_tx, monitor_rx) = mysten_metrics::metered_channel::channel( + 10000, + &mysten_metrics::get_metrics() + .unwrap() + .channel_inflight + .with_label_values(&["monitor_queue"]), + ); + let bridge_action_executor = BridgeActionExecutor::new( sui_client.clone(), - bridge_auth_agg, + bridge_auth_agg.clone(), store.clone(), client_config.key, client_config.sui_address, @@ -123,12 +133,16 @@ async fn start_client_components( ) .await; + let monitor = BridgeMonitor::new(sui_client.clone(), monitor_rx, bridge_auth_agg.clone()); + all_handles.push(spawn_logged_monitored_task!(monitor.run())); + let orchestrator = BridgeOrchestrator::new( sui_client, sui_events_rx, eth_events_rx, store.clone(), token_type_tags_tx, + monitor_tx, metrics, ); diff --git a/crates/sui-bridge/src/orchestrator.rs b/crates/sui-bridge/src/orchestrator.rs index b519b43573ff5..db437bb509017 100644 --- a/crates/sui-bridge/src/orchestrator.rs +++ b/crates/sui-bridge/src/orchestrator.rs @@ -31,6 +31,7 @@ pub struct BridgeOrchestrator { eth_events_rx: mysten_metrics::metered_channel::Receiver<(EthAddress, u64, Vec)>, store: Arc, token_type_tags_tx: tokio::sync::watch::Sender>, + monitor_tx: mysten_metrics::metered_channel::Sender, metrics: Arc, } @@ -44,6 +45,7 @@ where eth_events_rx: mysten_metrics::metered_channel::Receiver<(EthAddress, u64, Vec)>, store: Arc, token_type_tags_tx: tokio::sync::watch::Sender>, + monitor_tx: mysten_metrics::metered_channel::Sender, metrics: Arc, ) -> Self { Self { @@ -52,6 +54,7 @@ where eth_events_rx, store, token_type_tags_tx, + monitor_tx, metrics, } } @@ -74,6 +77,7 @@ where executor_sender_clone, self.sui_events_rx, self.token_type_tags_tx, + self.monitor_tx, metrics_clone, ))); let store_clone = self.store.clone(); @@ -106,6 +110,7 @@ where executor_tx: mysten_metrics::metered_channel::Sender, mut sui_events_rx: mysten_metrics::metered_channel::Receiver<(Identifier, Vec)>, token_type_tags_tx: tokio::sync::watch::Sender>, + monitor_tx: mysten_metrics::metered_channel::Sender, metrics: Arc, ) { info!("Starting sui watcher task"); @@ -150,7 +155,14 @@ where let bridge_event: SuiBridgeEvent = opt_bridge_event.unwrap(); info!("Observed Sui bridge event: {:?}", bridge_event); + // Send event to monitor + monitor_tx + .send(bridge_event.clone()) + .await + .expect("Sending event to monitor channel should not fail"); + // Handle NewTokenEvent + // TODO: broadcast this event and let the downstream services handle it if let SuiBridgeEvent::NewTokenEvent(e) = &bridge_event { if let std::collections::hash_map::Entry::Vacant(entry) = latest_token_config.entry(e.token_id) @@ -299,8 +311,16 @@ mod tests { // Note: this test may fail because of the following reasons: // the SuiEvent's struct tag does not match the ones in events.rs - let (sui_events_tx, sui_events_rx, _eth_events_tx, eth_events_rx, sui_client, store) = - setup(); + let ( + sui_events_tx, + sui_events_rx, + _eth_events_tx, + eth_events_rx, + monitor_tx, + _monitor_rx, + sui_client, + store, + ) = setup(); let (executor, mut executor_requested_action_rx) = MockExecutor::new(); let (token_type_tags_tx, _token_type_tags_rx) = tokio::sync::watch::channel(HashMap::new()); // start orchestrator @@ -312,6 +332,7 @@ mod tests { eth_events_rx, store.clone(), token_type_tags_tx, + monitor_tx, metrics, ) .run(executor) @@ -352,8 +373,16 @@ mod tests { #[tokio::test] async fn test_sui_watcher_task_add_new_token() { - let (sui_events_tx, sui_events_rx, _eth_events_tx, eth_events_rx, sui_client, store) = - setup(); + let ( + sui_events_tx, + sui_events_rx, + _eth_events_tx, + eth_events_rx, + monitor_tx, + _monitor_rx, + sui_client, + store, + ) = setup(); let (executor, _executor_requested_action_rx) = MockExecutor::new(); let (token_type_tags_tx, mut token_type_tags_rx) = @@ -367,6 +396,7 @@ mod tests { eth_events_rx, store.clone(), token_type_tags_tx, + monitor_tx, metrics, ) .run(executor) @@ -418,8 +448,16 @@ mod tests { // 1. Log and BridgeAction returned from `get_test_log_and_action` are not in sync // 2. Log returned from `get_test_log_and_action` is not parseable log (not abigen!, check abi.rs) - let (_sui_events_tx, sui_events_rx, eth_events_tx, eth_events_rx, sui_client, store) = - setup(); + let ( + _sui_events_tx, + sui_events_rx, + eth_events_tx, + eth_events_rx, + monitor_tx, + _monitor_rx, + sui_client, + store, + ) = setup(); let (token_type_tags_tx, _token_type_tags_rx) = tokio::sync::watch::channel(HashMap::new()); let (executor, mut executor_requested_action_rx) = MockExecutor::new(); // start orchestrator @@ -431,6 +469,7 @@ mod tests { eth_events_rx, store.clone(), token_type_tags_tx, + monitor_tx, metrics, ) .run(executor) @@ -481,8 +520,16 @@ mod tests { #[tokio::test] /// Test that when orchestrator starts, all pending actions are sent to executor async fn test_resume_actions_in_pending_logs() { - let (_sui_events_tx, sui_events_rx, _eth_events_tx, eth_events_rx, sui_client, store) = - setup(); + let ( + _sui_events_tx, + sui_events_rx, + _eth_events_tx, + eth_events_rx, + monitor_tx, + _monitor_rx, + sui_client, + store, + ) = setup(); let (executor, mut executor_requested_action_rx) = MockExecutor::new(); let (token_type_tags_tx, _token_type_tags_rx) = tokio::sync::watch::channel(HashMap::new()); @@ -510,6 +557,7 @@ mod tests { eth_events_rx, store.clone(), token_type_tags_tx, + monitor_tx, metrics, ) .run(executor) @@ -530,6 +578,8 @@ mod tests { mysten_metrics::metered_channel::Receiver<(Identifier, Vec)>, mysten_metrics::metered_channel::Sender<(EthAddress, u64, Vec)>, mysten_metrics::metered_channel::Receiver<(EthAddress, u64, Vec)>, + mysten_metrics::metered_channel::Sender, + mysten_metrics::metered_channel::Receiver, SuiClient, Arc, ) { @@ -560,12 +610,20 @@ mod tests { .channel_inflight .with_label_values(&["unit_test_sui_events_queue"]), ); - + let (monitor_tx, monitor_rx) = mysten_metrics::metered_channel::channel( + 10000, + &mysten_metrics::get_metrics() + .unwrap() + .channel_inflight + .with_label_values(&["monitor_queue"]), + ); ( sui_events_tx, sui_events_rx, eth_events_tx, eth_events_rx, + monitor_tx, + monitor_rx, sui_client, store, ) diff --git a/crates/sui-bridge/src/sui_mock_client.rs b/crates/sui-bridge/src/sui_mock_client.rs index 7f64be20003c4..835bd51eac7f9 100644 --- a/crates/sui-bridge/src/sui_mock_client.rs +++ b/crates/sui-bridge/src/sui_mock_client.rs @@ -12,7 +12,9 @@ use sui_json_rpc_types::SuiTransactionBlockResponse; use sui_json_rpc_types::{EventFilter, EventPage, SuiEvent}; use sui_types::base_types::ObjectID; use sui_types::base_types::ObjectRef; -use sui_types::bridge::{BridgeSummary, MoveTypeParsedTokenTransferMessage}; +use sui_types::bridge::{ + BridgeCommitteeSummary, BridgeSummary, MoveTypeParsedTokenTransferMessage, +}; use sui_types::digests::TransactionDigest; use sui_types::event::EventID; use sui_types::gas_coin::GasCoin; @@ -40,7 +42,7 @@ pub struct SuiMockClient { wildcard_transaction_response: Arc>>>, get_object_info: Arc>>, onchain_status: Arc>>, - + bridge_committee_summary: Arc>>, requested_transactions_tx: tokio::sync::broadcast::Sender, } @@ -56,6 +58,7 @@ impl SuiMockClient { wildcard_transaction_response: Default::default(), get_object_info: Default::default(), onchain_status: Default::default(), + bridge_committee_summary: Default::default(), requested_transactions_tx: tokio::sync::broadcast::channel(10000).0, } } @@ -105,6 +108,13 @@ impl SuiMockClient { .insert((action.chain_id() as u8, action.seq_number()), status); } + pub fn set_bridge_committee(&self, committee: BridgeCommitteeSummary) { + self.bridge_committee_summary + .lock() + .unwrap() + .replace(committee); + } + pub fn set_wildcard_transaction_response( &self, response: BridgeResult, @@ -200,7 +210,12 @@ impl SuiClientInner for SuiMockClient { bridge_records_id: ObjectID::random(), is_frozen: false, limiter: Default::default(), - committee: Default::default(), + committee: self + .bridge_committee_summary + .lock() + .unwrap() + .clone() + .unwrap_or_default(), treasury: Default::default(), }) } diff --git a/crates/sui-bridge/src/test_utils.rs b/crates/sui-bridge/src/test_utils.rs index 64d7b8ba02dc7..8b71664908777 100644 --- a/crates/sui-bridge/src/test_utils.rs +++ b/crates/sui-bridge/src/test_utils.rs @@ -7,7 +7,8 @@ use crate::events::SuiBridgeEvent; use crate::server::mock_handler::run_mock_server; use crate::sui_transaction_builder::build_sui_transaction; use crate::types::{ - BridgeCommitteeValiditySignInfo, CertifiedBridgeAction, VerifiedCertifiedBridgeAction, + BridgeCommittee, BridgeCommitteeValiditySignInfo, CertifiedBridgeAction, + VerifiedCertifiedBridgeAction, }; use crate::{ crypto::{BridgeAuthorityKeyPair, BridgeAuthorityPublicKey, BridgeAuthoritySignInfo}, @@ -38,7 +39,9 @@ use sui_sdk::wallet_context::WalletContext; use sui_test_transaction_builder::TestTransactionBuilder; use sui_types::base_types::ObjectRef; use sui_types::base_types::SequenceNumber; -use sui_types::bridge::{BridgeChainId, TOKEN_ID_USDC}; +use sui_types::bridge::MoveTypeCommitteeMember; +use sui_types::bridge::{BridgeChainId, BridgeCommitteeSummary, TOKEN_ID_USDC}; +use sui_types::crypto::ToFromBytes; use sui_types::object::Owner; use sui_types::transaction::{CallArg, ObjectArg}; use sui_types::{base_types::SuiAddress, crypto::get_key_pair, digests::TransactionDigest}; @@ -392,3 +395,29 @@ pub async fn approve_action_with_validator_secrets( expected_token_receiver ); } + +pub fn bridge_committee_to_bridge_committee_summary( + committee: BridgeCommittee, +) -> BridgeCommitteeSummary { + BridgeCommitteeSummary { + members: committee + .members() + .iter() + .map(|(k, v)| { + let bytes = k.as_bytes().to_vec(); + ( + bytes.clone(), + MoveTypeCommitteeMember { + sui_address: SuiAddress::random_for_testing_only(), + bridge_pubkey_bytes: bytes, + voting_power: v.voting_power, + http_rest_url: v.base_url.as_bytes().to_vec(), + blocklisted: v.is_blocklisted, + }, + ) + }) + .collect(), + member_registration: vec![], + last_committee_update_epoch: 0, + } +}