sui_bridge/server/
handler.rs

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