sui_bridge/server/
handler.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use super::governance_verifier::GovernanceVerifier;
5use crate::crypto::{BridgeAuthorityKeyPair, BridgeAuthoritySignInfo};
6use crate::error::{BridgeError, BridgeResult};
7use crate::eth_client::EthClient;
8use crate::sui_client::{SuiClient, SuiClientInner};
9use crate::types::{BridgeAction, SignedBridgeAction};
10use alloy::primitives::TxHash;
11use async_trait::async_trait;
12use axum::Json;
13use std::str::FromStr;
14use std::sync::Arc;
15use sui_types::digests::TransactionDigest;
16use tap::TapFallible;
17use tracing::info;
18
19#[async_trait]
20pub trait BridgeRequestHandlerTrait {
21    /// Handles a request to sign a BridgeAction that bridges assets
22    /// from Ethereum to Sui. The inputs are a transaction hash on Ethereum
23    /// that emitted the bridge event and the Event index in that transaction
24    async fn handle_eth_tx_hash(
25        &self,
26        tx_hash_hex: String,
27        event_idx: u16,
28    ) -> Result<Json<SignedBridgeAction>, BridgeError>;
29    /// Handles a request to sign a BridgeAction that bridges assets
30    /// from Sui to Ethereum. The inputs are a transaction digest on Sui
31    /// that emitted the bridge event and the Event index in that transaction
32    async fn handle_sui_tx_digest(
33        &self,
34        tx_digest_base58: String,
35        event_idx: u16,
36    ) -> Result<Json<SignedBridgeAction>, BridgeError>;
37
38    /// Handles a request to sign a BridgeAction that bridges assets
39    /// from Sui to Ethereum.
40    async fn handle_sui_token_transfer(
41        &self,
42        source_chain: u8,
43        message_type: u8,
44        bridge_seq_num: u64,
45    ) -> Result<Json<SignedBridgeAction>, BridgeError>;
46
47    /// Handles a request to sign a governance action.
48    async fn handle_governance_action(
49        &self,
50        action: BridgeAction,
51    ) -> Result<Json<SignedBridgeAction>, BridgeError>;
52}
53
54pub struct BridgeRequestHandler<SC> {
55    signer: Arc<BridgeAuthorityKeyPair>,
56    sui_client: Arc<SuiClient<SC>>,
57    eth_client: Arc<EthClient>,
58    governance_verifier: GovernanceVerifier,
59}
60
61impl<SC> BridgeRequestHandler<SC>
62where
63    SC: SuiClientInner + Send + Sync + 'static,
64{
65    pub fn new(
66        signer: BridgeAuthorityKeyPair,
67        sui_client: Arc<SuiClient<SC>>,
68        eth_client: Arc<EthClient>,
69        approved_governance_actions: Vec<BridgeAction>,
70    ) -> Self {
71        let signer = Arc::new(signer);
72
73        Self {
74            signer,
75            sui_client,
76            eth_client,
77            governance_verifier: GovernanceVerifier::new(approved_governance_actions).unwrap(),
78        }
79    }
80
81    fn sign(&self, bridge_action: BridgeAction) -> SignedBridgeAction {
82        let sig = BridgeAuthoritySignInfo::new(&bridge_action, &self.signer);
83        SignedBridgeAction::new_from_data_and_sig(bridge_action, sig)
84    }
85
86    async fn verify_eth(&self, key: (TxHash, u16)) -> BridgeResult<BridgeAction> {
87        let (tx_hash, event_idx) = key;
88        self.eth_client
89            .get_finalized_bridge_action_maybe(tx_hash, event_idx)
90            .await
91            .tap_ok(|action| info!("Eth action found: {:?}", action))
92    }
93
94    async fn verify_sui(&self, key: (TransactionDigest, u16)) -> BridgeResult<BridgeAction> {
95        let (tx_digest, event_idx) = key;
96        self.sui_client
97            .get_bridge_action_by_tx_digest_and_event_idx_maybe(&tx_digest, event_idx)
98            .await
99            .tap_ok(|action| info!("Sui action found: {:?}", action))
100    }
101
102    async fn verify_sui_message(
103        &self,
104        source_chain_id: u8,
105        _message_type: u8,
106        seq_number: u64,
107    ) -> BridgeResult<BridgeAction> {
108        let record = self
109            .sui_client
110            .get_bridge_record(source_chain_id, seq_number)
111            .await?
112            .ok_or_else(|| BridgeError::Generic(format!("message {seq_number} not found")))?;
113        if record.verified_signatures.is_some() {
114            return Err(BridgeError::Generic(format!(
115                "message {seq_number} already complete"
116            )));
117        }
118        BridgeAction::try_from_bridge_record(&record)
119            .tap_ok(|action| info!("Sui action found: {:?}", action))
120    }
121}
122
123#[async_trait]
124impl<SC> BridgeRequestHandlerTrait for BridgeRequestHandler<SC>
125where
126    SC: SuiClientInner + Send + Sync + 'static,
127{
128    async fn handle_eth_tx_hash(
129        &self,
130        tx_hash_hex: String,
131        event_idx: u16,
132    ) -> Result<Json<SignedBridgeAction>, BridgeError> {
133        let tx_hash = TxHash::from_str(&tx_hash_hex).map_err(|_| BridgeError::InvalidTxHash)?;
134        let bridge_action = self.verify_eth((tx_hash, event_idx)).await?;
135        Ok(Json(self.sign(bridge_action)))
136    }
137
138    async fn handle_sui_tx_digest(
139        &self,
140        tx_digest_base58: String,
141        event_idx: u16,
142    ) -> Result<Json<SignedBridgeAction>, BridgeError> {
143        let tx_digest = TransactionDigest::from_str(&tx_digest_base58)
144            .map_err(|_e| BridgeError::InvalidTxHash)?;
145
146        let bridge_action = self.verify_sui((tx_digest, event_idx)).await?;
147        Ok(Json(self.sign(bridge_action)))
148    }
149
150    async fn handle_sui_token_transfer(
151        &self,
152        source_chain: u8,
153        message_type: u8,
154        bridge_seq_num: u64,
155    ) -> Result<Json<SignedBridgeAction>, BridgeError> {
156        let bridge_action = self
157            .verify_sui_message(source_chain, message_type, bridge_seq_num)
158            .await?;
159        Ok(Json(self.sign(bridge_action)))
160    }
161
162    async fn handle_governance_action(
163        &self,
164        action: BridgeAction,
165    ) -> Result<Json<SignedBridgeAction>, BridgeError> {
166        if !action.is_governance_action() {
167            return Err(BridgeError::ActionIsNotGovernanceAction(Box::new(action)));
168        }
169        let bridge_action = self.governance_verifier.verify(action).await?;
170        Ok(Json(self.sign(bridge_action)))
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use std::collections::HashSet;
177
178    use super::*;
179    use crate::{
180        eth_mock_provider::EthMockService,
181        events::{MoveTokenDepositedEvent, SuiToEthTokenBridgeV1, init_all_struct_tags},
182        sui_mock_client::SuiMockClient,
183        test_utils::{
184            get_test_log_and_action, get_test_sui_to_eth_bridge_action, make_transaction_receipt,
185            mock_last_finalized_block,
186        },
187        types::{EmergencyAction, EmergencyActionType, LimitUpdateAction},
188    };
189    use alloy::{primitives::Address as EthAddress, rpc::types::TransactionReceipt};
190    use sui_json_rpc_types::{BcsEvent, SuiEvent};
191    use sui_types::bridge::{BridgeChainId, TOKEN_ID_USDC};
192    use sui_types::{base_types::SuiAddress, crypto::get_key_pair};
193
194    fn test_handler(
195        approved_actions: Vec<BridgeAction>,
196    ) -> (
197        BridgeRequestHandler<SuiMockClient>,
198        SuiMockClient,
199        EthMockService,
200        EthAddress,
201    ) {
202        let (_, kp): (_, BridgeAuthorityKeyPair) = get_key_pair();
203        let sui_client_mock = SuiMockClient::default();
204
205        let eth_mock_service = EthMockService::default();
206        let contract_address = EthAddress::random();
207        let eth_client = EthClient::new_mocked(
208            eth_mock_service.clone(),
209            HashSet::from_iter(vec![contract_address]),
210        );
211
212        let handler = BridgeRequestHandler::new(
213            kp,
214            Arc::new(SuiClient::new_for_testing(sui_client_mock.clone())),
215            Arc::new(eth_client),
216            approved_actions,
217        );
218        (handler, sui_client_mock, eth_mock_service, contract_address)
219    }
220
221    #[tokio::test]
222    async fn test_sui_verify() {
223        let (handler, sui_client_mock, _, _) = test_handler(vec![]);
224
225        let sui_tx_digest = TransactionDigest::random();
226        let sui_event_idx = 0;
227
228        // ensure we get an error
229        sui_client_mock.add_events_by_tx_digest_error(sui_tx_digest);
230        handler
231            .verify_sui((sui_tx_digest, sui_event_idx))
232            .await
233            .unwrap_err();
234
235        // Mock a cacheable error such as no bridge events in tx position (empty event list)
236        sui_client_mock.add_events_by_tx_digest(sui_tx_digest, vec![]);
237        assert!(matches!(
238            handler.verify_sui((sui_tx_digest, sui_event_idx)).await,
239            Err(BridgeError::NoBridgeEventsInTxPosition)
240        ));
241
242        // Test `sign` caches Ok result
243        let emitted_event_1 = MoveTokenDepositedEvent {
244            seq_num: 1,
245            source_chain: BridgeChainId::SuiCustom as u8,
246            sender_address: SuiAddress::random_for_testing_only().to_vec(),
247            target_chain: BridgeChainId::EthCustom as u8,
248            target_address: EthAddress::random().to_vec(),
249            token_type: TOKEN_ID_USDC,
250            amount_sui_adjusted: 12345,
251        };
252
253        init_all_struct_tags();
254
255        let mut sui_event_1 = SuiEvent::random_for_testing();
256        sui_event_1.type_ = SuiToEthTokenBridgeV1.get().unwrap().clone();
257        sui_event_1.bcs = BcsEvent::new(bcs::to_bytes(&emitted_event_1).unwrap());
258        let sui_tx_digest = sui_event_1.id.tx_digest;
259
260        let mut sui_event_2 = SuiEvent::random_for_testing();
261        sui_event_2.type_ = SuiToEthTokenBridgeV1.get().unwrap().clone();
262        sui_event_2.bcs = BcsEvent::new(bcs::to_bytes(&emitted_event_1).unwrap());
263        let sui_event_idx_2 = 1;
264        sui_client_mock.add_events_by_tx_digest(sui_tx_digest, vec![sui_event_2.clone()]);
265
266        sui_client_mock.add_events_by_tx_digest(
267            sui_tx_digest,
268            vec![sui_event_1.clone(), sui_event_2.clone()],
269        );
270        handler
271            .verify_sui((sui_tx_digest, sui_event_idx))
272            .await
273            .unwrap();
274        handler
275            .verify_sui((sui_tx_digest, sui_event_idx_2))
276            .await
277            .unwrap();
278    }
279
280    #[tokio::test]
281    async fn test_eth_verify() {
282        let (handler, _sui_client_mock, eth_mock_service, contract_address) = test_handler(vec![]);
283
284        // Test `sign` Ok result
285        let eth_tx_hash = TxHash::random();
286        let eth_event_idx = 0;
287        let (log, _action) = get_test_log_and_action(contract_address, eth_tx_hash, eth_event_idx);
288        eth_mock_service
289            .add_response::<[TxHash; 1], TransactionReceipt, TransactionReceipt>(
290                "eth_getTransactionReceipt",
291                [log.transaction_hash.unwrap()],
292                make_transaction_receipt(
293                    EthAddress::default(),
294                    log.block_number,
295                    vec![log.clone()],
296                ),
297            )
298            .unwrap();
299        mock_last_finalized_block(&eth_mock_service, log.block_number.unwrap());
300        // Mock eth_getBlockByNumber for the block timestamp fetch
301        eth_mock_service
302            .add_response(
303                "eth_getBlockByNumber",
304                (format!("0x{:x}", log.block_number.unwrap()), false),
305                make_transaction_receipt(
306                    EthAddress::default(),
307                    log.block_number,
308                    vec![log.clone()],
309                ),
310            )
311            .unwrap();
312
313        handler
314            .verify_eth((eth_tx_hash, eth_event_idx))
315            .await
316            .unwrap();
317    }
318
319    #[tokio::test]
320    async fn test_signer_with_governance_verifier() {
321        let action_1 = BridgeAction::EmergencyAction(EmergencyAction {
322            chain_id: BridgeChainId::EthCustom,
323            nonce: 1,
324            action_type: EmergencyActionType::Pause,
325        });
326        let action_2 = BridgeAction::LimitUpdateAction(LimitUpdateAction {
327            chain_id: BridgeChainId::EthCustom,
328            sending_chain_id: BridgeChainId::SuiCustom,
329            nonce: 1,
330            new_usd_limit: 10000,
331        });
332
333        let (handler, _, _, _) = test_handler(vec![action_1.clone(), action_2.clone()]);
334        let verifier = handler.governance_verifier;
335        assert_eq!(
336            verifier.verify(action_1.clone()).await.unwrap(),
337            action_1.clone()
338        );
339        assert_eq!(
340            verifier.verify(action_2.clone()).await.unwrap(),
341            action_2.clone()
342        );
343
344        // alter action_1 to action_3
345        let action_3 = BridgeAction::EmergencyAction(EmergencyAction {
346            chain_id: BridgeChainId::EthCustom,
347            nonce: 1,
348            action_type: EmergencyActionType::Unpause,
349        });
350        // action_3 is not signable
351        assert!(matches!(
352            verifier.verify(action_3.clone()).await.unwrap_err(),
353            BridgeError::GovernanceActionIsNotApproved
354        ));
355
356        // Non governance action is not signable
357        let action_4 = get_test_sui_to_eth_bridge_action(None, None, None, None, None, None, None);
358        assert!(matches!(
359            verifier.verify(action_4.clone()).await.unwrap_err(),
360            BridgeError::ActionIsNotGovernanceAction(..)
361        ));
362    }
363}