sui_bridge/server/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(clippy::inconsistent_digit_grouping)]
5use crate::crypto::BridgeAuthorityPublicKeyBytes;
6use crate::error::BridgeError;
7use crate::metrics::BridgeMetrics;
8use crate::server::handler::BridgeRequestHandlerTrait;
9use crate::types::{
10    AddTokensOnEvmAction, AddTokensOnSuiAction, AssetPriceUpdateAction, BlocklistCommitteeAction,
11    BlocklistType, BridgeAction, EmergencyAction, EmergencyActionType, EvmContractUpgradeAction,
12    LimitUpdateAction, SignedBridgeAction,
13};
14use crate::with_metrics;
15use alloy::primitives::Address as EthAddress;
16use axum::Json;
17use axum::Router;
18use axum::extract::{Path, Request, State};
19use axum::http::StatusCode;
20use axum::middleware::{self, Next};
21use axum::response::{IntoResponse, Response};
22use axum::routing::get;
23use fastcrypto::ed25519::Ed25519PublicKey;
24use fastcrypto::encoding::{Encoding, Hex};
25use fastcrypto::traits::ToFromBytes;
26use std::net::SocketAddr;
27use std::str::FromStr;
28use std::sync::Arc;
29use sui_types::TypeTag;
30use sui_types::bridge::BridgeChainId;
31use tracing::{info, instrument};
32
33pub mod governance_verifier;
34pub mod handler;
35
36#[cfg(any(feature = "test-utils", test))]
37pub(crate) mod mock_handler;
38
39pub const APPLICATION_JSON: &str = "application/json";
40
41pub const MAX_REQUEST_URI_SIZE: usize = 8 * 1024;
42pub const MAX_REQUEST_BODY_SIZE: usize = 64 * 1024;
43
44// Maximum number of items allowed in comma-separated lists in governance endpoints
45// This prevents DoS attacks where oversized lists cause panics during u8 conversion
46pub const MAX_LIST_SIZE: usize = 255;
47
48pub const PING_PATH: &str = "/ping";
49pub const METRICS_KEY_PATH: &str = "/metrics_pub_key";
50
51// Important: for BridgeActions, the paths need to match the ones in bridge_client.rs
52pub const ETH_TO_SUI_TX_PATH: &str = "/sign/bridge_tx/eth/sui/{tx_hash}/{event_index}";
53pub const SUI_TO_ETH_TX_PATH: &str = "/sign/bridge_tx/sui/eth/{tx_digest}/{event_index}";
54pub const SUI_TO_ETH_TRANSFER_PATH: &str =
55    "/sign/bridge_action/sui/eth/{source_chain}/{message_type}/{bridge_seq_num}";
56pub const COMMITTEE_BLOCKLIST_UPDATE_PATH: &str =
57    "/sign/update_committee_blocklist/{chain_id}/{nonce}/{type}/{keys}";
58pub const EMERGENCY_BUTTON_PATH: &str = "/sign/emergency_button/{chain_id}/{nonce}/{type}";
59pub const LIMIT_UPDATE_PATH: &str =
60    "/sign/update_limit/{chain_id}/{nonce}/{sending_chain_id}/{new_usd_limit}";
61pub const ASSET_PRICE_UPDATE_PATH: &str =
62    "/sign/update_asset_price/{chain_id}/{nonce}/{token_id}/{new_usd_price}";
63pub const EVM_CONTRACT_UPGRADE_PATH_WITH_CALLDATA: &str =
64    "/sign/upgrade_evm_contract/{chain_id}/{nonce}/{proxy_address}/{new_impl_address}/{calldata}";
65pub const EVM_CONTRACT_UPGRADE_PATH: &str =
66    "/sign/upgrade_evm_contract/{chain_id}/{nonce}/{proxy_address}/{new_impl_address}";
67pub const ADD_TOKENS_ON_SUI_PATH: &str = "/sign/add_tokens_on_sui/{chain_id}/{nonce}/{native}/{token_ids}/{token_type_names}/{token_prices}";
68pub const ADD_TOKENS_ON_EVM_PATH: &str = "/sign/add_tokens_on_evm/{chain_id}/{nonce}/{native}/{token_ids}/{token_addresses}/{token_sui_decimals}/{token_prices}";
69
70// BridgeNode's public metadata that is accessible via the `/ping` endpoint.
71// Be careful with what to put here, as it is public.
72#[derive(serde::Serialize)]
73pub struct BridgeNodePublicMetadata {
74    pub version: &'static str,
75    pub metrics_pubkey: Option<Arc<Ed25519PublicKey>>,
76}
77
78impl BridgeNodePublicMetadata {
79    pub fn new(version: &'static str, metrics_pubkey: Ed25519PublicKey) -> Self {
80        Self {
81            version,
82            metrics_pubkey: Some(metrics_pubkey.into()),
83        }
84    }
85
86    pub fn empty_for_testing() -> Self {
87        Self {
88            version: "testing",
89            metrics_pubkey: None,
90        }
91    }
92}
93
94pub fn run_server(
95    socket_address: &SocketAddr,
96    handler: impl BridgeRequestHandlerTrait + Sync + Send + 'static,
97    metrics: Arc<BridgeMetrics>,
98    metadata: Arc<BridgeNodePublicMetadata>,
99) -> tokio::task::JoinHandle<()> {
100    let socket_address = *socket_address;
101    tokio::spawn(async move {
102        let listener = tokio::net::TcpListener::bind(socket_address).await.unwrap();
103        axum::serve(
104            listener,
105            make_router(Arc::new(handler), metrics, metadata).into_make_service(),
106        )
107        .await
108        .unwrap();
109    })
110}
111
112pub(crate) fn make_router(
113    handler: Arc<impl BridgeRequestHandlerTrait + Sync + Send + 'static>,
114    metrics: Arc<BridgeMetrics>,
115    metadata: Arc<BridgeNodePublicMetadata>,
116) -> Router {
117    Router::new()
118        .route("/", get(health_check))
119        .route(PING_PATH, get(ping))
120        .route(METRICS_KEY_PATH, get(metrics_key_fetch))
121        .route(ETH_TO_SUI_TX_PATH, get(handle_eth_tx_hash))
122        .route(SUI_TO_ETH_TX_PATH, get(handle_sui_tx_digest))
123        .route(SUI_TO_ETH_TRANSFER_PATH, get(handle_sui_token_transfer))
124        .route(
125            COMMITTEE_BLOCKLIST_UPDATE_PATH,
126            get(handle_update_committee_blocklist_action),
127        )
128        .route(EMERGENCY_BUTTON_PATH, get(handle_emergency_action))
129        .route(LIMIT_UPDATE_PATH, get(handle_limit_update_action))
130        .route(
131            ASSET_PRICE_UPDATE_PATH,
132            get(handle_asset_price_update_action),
133        )
134        .route(EVM_CONTRACT_UPGRADE_PATH, get(handle_evm_contract_upgrade))
135        .route(
136            EVM_CONTRACT_UPGRADE_PATH_WITH_CALLDATA,
137            get(handle_evm_contract_upgrade_with_calldata),
138        )
139        .route(ADD_TOKENS_ON_SUI_PATH, get(handle_add_tokens_on_sui))
140        .route(ADD_TOKENS_ON_EVM_PATH, get(handle_add_tokens_on_evm))
141        .layer(middleware::from_fn(reject_oversized_requests))
142        .with_state((handler, metrics, metadata))
143}
144
145async fn reject_oversized_requests(req: Request, next: Next) -> Response {
146    let uri_len = req
147        .uri()
148        .path_and_query()
149        .map(|v| v.as_str().len())
150        .unwrap_or(0);
151    if uri_len > MAX_REQUEST_URI_SIZE {
152        return StatusCode::URI_TOO_LONG.into_response();
153    }
154
155    if let Some(content_length) = req.headers().get(axum::http::header::CONTENT_LENGTH) {
156        let body_len = content_length
157            .to_str()
158            .ok()
159            .and_then(|v| v.parse::<usize>().ok());
160
161        match body_len {
162            Some(size) if size > MAX_REQUEST_BODY_SIZE => {
163                return StatusCode::PAYLOAD_TOO_LARGE.into_response();
164            }
165            None => {
166                return StatusCode::BAD_REQUEST.into_response();
167            }
168            _ => {}
169        }
170    }
171
172    next.run(req).await
173}
174
175impl axum::response::IntoResponse for BridgeError {
176    fn into_response(self) -> axum::response::Response {
177        let status = match &self {
178            BridgeError::InvalidTxHash
179            | BridgeError::UnknownTokenId(_)
180            | BridgeError::InvalidBridgeClientRequest(_)
181            | BridgeError::InvalidChainId
182            | BridgeError::ActionIsNotGovernanceAction(_)
183            | BridgeError::ActionIsNotTokenTransferAction
184            | BridgeError::GovernanceActionIsNotApproved => StatusCode::BAD_REQUEST,
185            BridgeError::TxNotFound | BridgeError::NoBridgeEventsInTxPosition => {
186                StatusCode::NOT_FOUND
187            }
188            BridgeError::TxNotFinalized => StatusCode::CONFLICT,
189            BridgeError::TransientProviderError(_) => StatusCode::SERVICE_UNAVAILABLE,
190            _ => StatusCode::INTERNAL_SERVER_ERROR,
191        };
192
193        let sanitized_error = match self {
194            BridgeError::InvalidTxHash => "InvalidTxHash",
195            BridgeError::OriginTxFailed => "OriginTxFailed",
196            BridgeError::TxNotFound => "TxNotFound",
197            BridgeError::TxNotFinalized => "TxNotFinalized",
198            BridgeError::NoBridgeEventsInTxPosition => "NoBridgeEventsInTxPosition",
199            BridgeError::BridgeEventInUnrecognizedEthContract => {
200                "BridgeEventInUnrecognizedEthContract"
201            }
202            BridgeError::BridgeEventInUnrecognizedSuiPackage => {
203                "BridgeEventInUnrecognizedSuiPackage"
204            }
205            BridgeError::BridgeEventNotActionable => "BridgeEventNotActionable",
206            BridgeError::UnknownTokenId(_) => "UnknownTokenId",
207            BridgeError::InvalidBridgeCommittee(_) => "InvalidBridgeCommittee",
208            BridgeError::InvalidBridgeAuthoritySignature(_) => "InvalidBridgeAuthoritySignature",
209            BridgeError::InvalidBridgeAuthority(_) => "InvalidBridgeAuthority",
210            BridgeError::InvalidAuthorityUrl(_) => "InvalidAuthorityUrl",
211            BridgeError::InvalidBridgeClientRequest(_) => "InvalidBridgeClientRequest",
212            BridgeError::InvalidChainId => "InvalidChainId",
213            BridgeError::MismatchedAuthoritySigner => "MismatchedAuthoritySigner",
214            BridgeError::MismatchedAction => "MismatchedAction",
215            BridgeError::ActionIsNotGovernanceAction(_) => "ActionIsNotGovernanceAction",
216            BridgeError::GovernanceActionIsNotApproved => "GovernanceActionIsNotApproved",
217            BridgeError::AuthoirtyUrlInvalid => "AuthoirtyUrlInvalid",
218            BridgeError::ActionIsNotTokenTransferAction => "ActionIsNotTokenTransferAction",
219            BridgeError::TransientProviderError(_) => "TransientProviderError",
220            _ => "InternalError",
221        };
222
223        (status, format!("BridgeError::{sanitized_error}")).into_response()
224    }
225}
226
227impl<E> From<E> for BridgeError
228where
229    E: Into<anyhow::Error>,
230{
231    fn from(err: E) -> Self {
232        Self::Generic(err.into().to_string())
233    }
234}
235
236async fn health_check() -> StatusCode {
237    StatusCode::OK
238}
239
240/// Validates that a comma-separated list doesn't exceed the maximum allowed size
241/// to prevent DoS attacks during u8 conversion in encoding
242fn validate_list_size(list_str: &str, field_name: &str) -> Result<(), BridgeError> {
243    let count = list_str.split(',').count();
244    if count > MAX_LIST_SIZE {
245        return Err(BridgeError::InvalidBridgeClientRequest(format!(
246            "{} list size {} exceeds maximum allowed size of {}",
247            field_name, count, MAX_LIST_SIZE
248        )));
249    }
250    Ok(())
251}
252
253async fn ping(
254    State((_handler, _metrics, metadata)): State<(
255        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
256        Arc<BridgeMetrics>,
257        Arc<BridgeNodePublicMetadata>,
258    )>,
259) -> Result<Json<Arc<BridgeNodePublicMetadata>>, BridgeError> {
260    Ok(Json(metadata))
261}
262
263async fn metrics_key_fetch(
264    State((_handler, _metrics, metadata)): State<(
265        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
266        Arc<BridgeMetrics>,
267        Arc<BridgeNodePublicMetadata>,
268    )>,
269) -> Result<Json<Option<Arc<Ed25519PublicKey>>>, BridgeError> {
270    Ok(Json(metadata.metrics_pubkey.clone()))
271}
272
273#[instrument(level = "error", skip_all, fields(tx_hash_hex=tx_hash_hex, event_idx=event_idx))]
274async fn handle_eth_tx_hash(
275    Path((tx_hash_hex, event_idx)): Path<(String, u16)>,
276    State((handler, metrics, _metadata)): State<(
277        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
278        Arc<BridgeMetrics>,
279        Arc<BridgeNodePublicMetadata>,
280    )>,
281) -> Result<Json<SignedBridgeAction>, BridgeError> {
282    let future = async {
283        let sig = handler.handle_eth_tx_hash(tx_hash_hex, event_idx).await?;
284        Ok(sig)
285    };
286    with_metrics!(metrics.clone(), "handle_eth_tx_hash", future).await
287}
288
289#[instrument(level = "error", skip_all, fields(tx_digest_base58=tx_digest_base58, event_idx=event_idx))]
290async fn handle_sui_tx_digest(
291    Path((tx_digest_base58, event_idx)): Path<(String, u16)>,
292    State((handler, metrics, _metadata)): State<(
293        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
294        Arc<BridgeMetrics>,
295        Arc<BridgeNodePublicMetadata>,
296    )>,
297) -> Result<Json<SignedBridgeAction>, BridgeError> {
298    let future = async {
299        let sig: Json<SignedBridgeAction> = handler
300            .handle_sui_tx_digest(tx_digest_base58, event_idx)
301            .await?;
302        Ok(sig)
303    };
304    with_metrics!(metrics.clone(), "handle_sui_tx_digest", future).await
305}
306
307#[instrument(level = "error", skip_all, fields(source_chain=source_chain, message_type=message_type, bridge_seq_num=bridge_seq_num))]
308async fn handle_sui_token_transfer(
309    Path((source_chain, message_type, bridge_seq_num)): Path<(u8, u8, u64)>,
310    State((handler, metrics, _metadata)): State<(
311        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
312        Arc<BridgeMetrics>,
313        Arc<BridgeNodePublicMetadata>,
314    )>,
315) -> Result<Json<SignedBridgeAction>, BridgeError> {
316    let future = async {
317        let sig: Json<SignedBridgeAction> = handler
318            .handle_sui_token_transfer(source_chain, message_type, bridge_seq_num)
319            .await?;
320        Ok(sig)
321    };
322    with_metrics!(metrics.clone(), "handle_sui_token_transfer", future).await
323}
324
325#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, blocklist_type=blocklist_type, keys=keys))]
326async fn handle_update_committee_blocklist_action(
327    Path((chain_id, nonce, blocklist_type, keys)): Path<(u8, u64, u8, String)>,
328    State((handler, metrics, _metadata)): State<(
329        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
330        Arc<BridgeMetrics>,
331        Arc<BridgeNodePublicMetadata>,
332    )>,
333) -> Result<Json<SignedBridgeAction>, BridgeError> {
334    let future = async {
335        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
336            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
337        })?;
338        let blocklist_type = BlocklistType::try_from(blocklist_type).map_err(|err| {
339            BridgeError::InvalidBridgeClientRequest(format!(
340                "Invalid blocklist action type: {:?}",
341                err
342            ))
343        })?;
344        // Validate list size to prevent DoS
345        validate_list_size(&keys, "keys")?;
346        let members_to_update = keys
347            .split(',')
348            .map(|s| {
349                let bytes = Hex::decode(s).map_err(|e| anyhow::anyhow!("{:?}", e))?;
350                BridgeAuthorityPublicKeyBytes::from_bytes(&bytes)
351                    .map_err(|e| anyhow::anyhow!("{:?}", e))
352            })
353            .collect::<Result<Vec<_>, _>>()
354            .map_err(|e| BridgeError::InvalidBridgeClientRequest(format!("{:?}", e)))?;
355        let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
356            chain_id,
357            nonce,
358            blocklist_type,
359            members_to_update,
360        });
361
362        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
363        Ok(sig)
364    };
365    with_metrics!(
366        metrics.clone(),
367        "handle_update_committee_blocklist_action",
368        future
369    )
370    .await
371}
372
373#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, action_type=action_type))]
374async fn handle_emergency_action(
375    Path((chain_id, nonce, action_type)): Path<(u8, u64, u8)>,
376    State((handler, metrics, _metadata)): State<(
377        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
378        Arc<BridgeMetrics>,
379        Arc<BridgeNodePublicMetadata>,
380    )>,
381) -> Result<Json<SignedBridgeAction>, BridgeError> {
382    let future = async {
383        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
384            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
385        })?;
386        let action_type = EmergencyActionType::try_from(action_type).map_err(|err| {
387            BridgeError::InvalidBridgeClientRequest(format!(
388                "Invalid emergency action type: {:?}",
389                err
390            ))
391        })?;
392        let action = BridgeAction::EmergencyAction(EmergencyAction {
393            chain_id,
394            nonce,
395            action_type,
396        });
397        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
398        Ok(sig)
399    };
400    with_metrics!(metrics.clone(), "handle_emergency_action", future).await
401}
402
403#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, sending_chain_id=sending_chain_id, new_usd_limit=new_usd_limit))]
404async fn handle_limit_update_action(
405    Path((chain_id, nonce, sending_chain_id, new_usd_limit)): Path<(u8, u64, u8, u64)>,
406    State((handler, metrics, _metadata)): State<(
407        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
408        Arc<BridgeMetrics>,
409        Arc<BridgeNodePublicMetadata>,
410    )>,
411) -> Result<Json<SignedBridgeAction>, BridgeError> {
412    let future = async {
413        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
414            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
415        })?;
416        let sending_chain_id = BridgeChainId::try_from(sending_chain_id).map_err(|err| {
417            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
418        })?;
419        let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
420            chain_id,
421            nonce,
422            sending_chain_id,
423            new_usd_limit,
424        });
425        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
426        Ok(sig)
427    };
428    with_metrics!(metrics.clone(), "handle_limit_update_action", future).await
429}
430
431#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, token_id=token_id, new_usd_price=new_usd_price))]
432async fn handle_asset_price_update_action(
433    Path((chain_id, nonce, token_id, new_usd_price)): Path<(u8, u64, u8, u64)>,
434    State((handler, metrics, _metadata)): State<(
435        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
436        Arc<BridgeMetrics>,
437        Arc<BridgeNodePublicMetadata>,
438    )>,
439) -> Result<Json<SignedBridgeAction>, BridgeError> {
440    let future = async {
441        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
442            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
443        })?;
444        let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
445            chain_id,
446            nonce,
447            token_id,
448            new_usd_price,
449        });
450        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
451        Ok(sig)
452    };
453    with_metrics!(metrics.clone(), "handle_asset_price_update_action", future).await
454}
455
456#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, proxy_address=format!("{:x}", proxy_address), new_impl_address=format!("{:x}", new_impl_address)))]
457async fn handle_evm_contract_upgrade_with_calldata(
458    Path((chain_id, nonce, proxy_address, new_impl_address, calldata)): Path<(
459        u8,
460        u64,
461        EthAddress,
462        EthAddress,
463        String,
464    )>,
465    State((handler, metrics, _metadata)): State<(
466        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
467        Arc<BridgeMetrics>,
468        Arc<BridgeNodePublicMetadata>,
469    )>,
470) -> Result<Json<SignedBridgeAction>, BridgeError> {
471    let future = async {
472        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
473            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
474        })?;
475        let call_data = Hex::decode(&calldata).map_err(|e| {
476            BridgeError::InvalidBridgeClientRequest(format!("Invalid call data: {:?}", e))
477        })?;
478        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
479            chain_id,
480            nonce,
481            proxy_address,
482            new_impl_address,
483            call_data,
484        });
485        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
486        Ok(sig)
487    };
488    with_metrics!(
489        metrics.clone(),
490        "handle_evm_contract_upgrade_with_calldata",
491        future
492    )
493    .await
494}
495
496#[instrument(
497    level = "error",
498    skip_all,
499    fields(chain_id, nonce, proxy_address, new_impl_address)
500)]
501async fn handle_evm_contract_upgrade(
502    Path((chain_id, nonce, proxy_address, new_impl_address)): Path<(
503        u8,
504        u64,
505        EthAddress,
506        EthAddress,
507    )>,
508    State((handler, metrics, _metadata)): State<(
509        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
510        Arc<BridgeMetrics>,
511        Arc<BridgeNodePublicMetadata>,
512    )>,
513) -> Result<Json<SignedBridgeAction>, BridgeError> {
514    let future = async {
515        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
516            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
517        })?;
518        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
519            chain_id,
520            nonce,
521            proxy_address,
522            new_impl_address,
523            call_data: vec![],
524        });
525        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
526
527        Ok(sig)
528    };
529    with_metrics!(metrics.clone(), "handle_evm_contract_upgrade", future).await
530}
531
532#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, native=native, token_ids=token_ids, token_type_names=token_type_names, token_prices=token_prices))]
533async fn handle_add_tokens_on_sui(
534    Path((chain_id, nonce, native, token_ids, token_type_names, token_prices)): Path<(
535        u8,
536        u64,
537        u8,
538        String,
539        String,
540        String,
541    )>,
542    State((handler, metrics, _metadata)): State<(
543        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
544        Arc<BridgeMetrics>,
545        Arc<BridgeNodePublicMetadata>,
546    )>,
547) -> Result<Json<SignedBridgeAction>, BridgeError> {
548    let future = async {
549        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
550            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
551        })?;
552
553        if !chain_id.is_sui_chain() {
554            return Err(BridgeError::InvalidBridgeClientRequest(
555                "handle_add_tokens_on_sui only expects Sui chain id".to_string(),
556            ));
557        }
558
559        let native = match native {
560            1 => true,
561            0 => false,
562            _ => {
563                return Err(BridgeError::InvalidBridgeClientRequest(format!(
564                    "Invalid native flag: {}",
565                    native
566                )));
567            }
568        };
569        // Validate list sizes to prevent DoS
570        validate_list_size(&token_ids, "token_ids")?;
571        validate_list_size(&token_type_names, "token_type_names")?;
572        validate_list_size(&token_prices, "token_prices")?;
573
574        let token_ids = token_ids
575            .split(',')
576            .map(|s| {
577                s.parse::<u8>().map_err(|err| {
578                    BridgeError::InvalidBridgeClientRequest(format!("Invalid token id: {:?}", err))
579                })
580            })
581            .collect::<Result<Vec<_>, _>>()?;
582        let token_type_names = token_type_names
583            .split(',')
584            .map(|s| {
585                TypeTag::from_str(s).map_err(|err| {
586                    BridgeError::InvalidBridgeClientRequest(format!(
587                        "Invalid token type name: {:?}",
588                        err
589                    ))
590                })
591            })
592            .collect::<Result<Vec<_>, _>>()?;
593        let token_prices = token_prices
594            .split(',')
595            .map(|s| {
596                s.parse::<u64>().map_err(|err| {
597                    BridgeError::InvalidBridgeClientRequest(format!(
598                        "Invalid token price: {:?}",
599                        err
600                    ))
601                })
602            })
603            .collect::<Result<Vec<_>, _>>()?;
604        let action = BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction {
605            chain_id,
606            nonce,
607            native,
608            token_ids,
609            token_type_names,
610            token_prices,
611        });
612        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
613        Ok(sig)
614    };
615    with_metrics!(metrics.clone(), "handle_add_tokens_on_sui", future).await
616}
617
618#[instrument(level = "error", skip_all, fields(chain_id=chain_id, nonce=nonce, native=native, token_ids=token_ids, token_addresses=token_addresses, token_sui_decimals=token_sui_decimals, token_prices=token_prices))]
619async fn handle_add_tokens_on_evm(
620    Path((chain_id, nonce, native, token_ids, token_addresses, token_sui_decimals, token_prices)): Path<(
621        u8,
622        u64,
623        u8,
624        String,
625        String,
626        String,
627        String,
628    )>,
629    State((handler, metrics, _metadata)): State<(
630        Arc<impl BridgeRequestHandlerTrait + Sync + Send>,
631        Arc<BridgeMetrics>,
632        Arc<BridgeNodePublicMetadata>,
633    )>,
634) -> Result<Json<SignedBridgeAction>, BridgeError> {
635    let future = async {
636        let chain_id = BridgeChainId::try_from(chain_id).map_err(|err| {
637            BridgeError::InvalidBridgeClientRequest(format!("Invalid chain id: {:?}", err))
638        })?;
639        if chain_id.is_sui_chain() {
640            return Err(BridgeError::InvalidBridgeClientRequest(
641                "handle_add_tokens_on_evm does not expect Sui chain id".to_string(),
642            ));
643        }
644
645        let native = match native {
646            1 => true,
647            0 => false,
648            _ => {
649                return Err(BridgeError::InvalidBridgeClientRequest(format!(
650                    "Invalid native flag: {}",
651                    native
652                )));
653            }
654        };
655        // Validate list sizes to prevent DoS
656        validate_list_size(&token_ids, "token_ids")?;
657        validate_list_size(&token_addresses, "token_addresses")?;
658        validate_list_size(&token_sui_decimals, "token_sui_decimals")?;
659        validate_list_size(&token_prices, "token_prices")?;
660
661        let token_ids = token_ids
662            .split(',')
663            .map(|s| {
664                s.parse::<u8>().map_err(|err| {
665                    BridgeError::InvalidBridgeClientRequest(format!("Invalid token id: {:?}", err))
666                })
667            })
668            .collect::<Result<Vec<_>, _>>()?;
669        let token_addresses = token_addresses
670            .split(',')
671            .map(|s| {
672                EthAddress::from_str(s).map_err(|err| {
673                    BridgeError::InvalidBridgeClientRequest(format!(
674                        "Invalid token address: {:?}",
675                        err
676                    ))
677                })
678            })
679            .collect::<Result<Vec<_>, _>>()?;
680        let token_sui_decimals = token_sui_decimals
681            .split(',')
682            .map(|s| {
683                s.parse::<u8>().map_err(|err| {
684                    BridgeError::InvalidBridgeClientRequest(format!(
685                        "Invalid token sui decimals: {:?}",
686                        err
687                    ))
688                })
689            })
690            .collect::<Result<Vec<_>, _>>()?;
691        let token_prices = token_prices
692            .split(',')
693            .map(|s| {
694                s.parse::<u64>().map_err(|err| {
695                    BridgeError::InvalidBridgeClientRequest(format!(
696                        "Invalid token price: {:?}",
697                        err
698                    ))
699                })
700            })
701            .collect::<Result<Vec<_>, _>>()?;
702        let action = BridgeAction::AddTokensOnEvmAction(AddTokensOnEvmAction {
703            chain_id,
704            nonce,
705            native,
706            token_ids,
707            token_addresses,
708            token_sui_decimals,
709            token_prices,
710        });
711        let sig: Json<SignedBridgeAction> = handler.handle_governance_action(action).await?;
712        Ok(sig)
713    };
714    with_metrics!(metrics.clone(), "handle_add_tokens_on_evm", future).await
715}
716
717#[macro_export]
718macro_rules! with_metrics {
719    ($metrics:expr, $type_:expr, $func:expr) => {
720        async move {
721            info!("Received {} request", $type_);
722            $metrics
723                .requests_received
724                .with_label_values(&[$type_])
725                .inc();
726            $metrics
727                .requests_inflight
728                .with_label_values(&[$type_])
729                .inc();
730
731            let result = $func.await;
732
733            match &result {
734                Ok(_) => {
735                    info!("{} request succeeded", $type_);
736                    $metrics.requests_ok.with_label_values(&[$type_]).inc();
737                }
738                Err(e) => {
739                    info!("{} request failed: {:?}", $type_, e);
740                    $metrics.err_requests.with_label_values(&[$type_]).inc();
741                }
742            }
743
744            $metrics
745                .requests_inflight
746                .with_label_values(&[$type_])
747                .dec();
748            result
749        }
750    };
751}
752
753#[cfg(test)]
754mod tests {
755    use sui_types::bridge::TOKEN_ID_BTC;
756
757    use super::*;
758    use crate::client::bridge_client::BridgeClient;
759    use crate::server::mock_handler::BridgeRequestMockHandler;
760    use crate::test_utils::get_test_authorities_and_run_mock_bridge_server;
761    use crate::types::BridgeCommittee;
762    use axum::response::IntoResponse;
763    use reqwest::header::CONTENT_LENGTH;
764
765    #[tokio::test]
766    async fn test_bridge_server_handle_blocklist_update_action_path() {
767        let client = setup();
768
769        let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes(
770            &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4")
771                .unwrap(),
772        )
773        .unwrap();
774        let action = BridgeAction::BlocklistCommitteeAction(BlocklistCommitteeAction {
775            nonce: 129,
776            chain_id: BridgeChainId::SuiCustom,
777            blocklist_type: BlocklistType::Blocklist,
778            members_to_update: vec![pub_key_bytes.clone()],
779        });
780        client.request_sign_bridge_action(action).await.unwrap();
781    }
782
783    #[tokio::test]
784    async fn test_bridge_server_handle_emergency_action_path() {
785        let client = setup();
786
787        let action = BridgeAction::EmergencyAction(EmergencyAction {
788            nonce: 55,
789            chain_id: BridgeChainId::SuiCustom,
790            action_type: EmergencyActionType::Pause,
791        });
792        client.request_sign_bridge_action(action).await.unwrap();
793    }
794
795    #[tokio::test]
796    async fn test_bridge_server_handle_limit_update_action_path() {
797        let client = setup();
798
799        let action = BridgeAction::LimitUpdateAction(LimitUpdateAction {
800            nonce: 15,
801            chain_id: BridgeChainId::SuiCustom,
802            sending_chain_id: BridgeChainId::EthCustom,
803            new_usd_limit: 1_000_000_0000, // $1M USD
804        });
805        client.request_sign_bridge_action(action).await.unwrap();
806    }
807
808    #[tokio::test]
809    async fn test_bridge_server_handle_asset_price_update_action_path() {
810        let client = setup();
811
812        let action = BridgeAction::AssetPriceUpdateAction(AssetPriceUpdateAction {
813            nonce: 266,
814            chain_id: BridgeChainId::SuiCustom,
815            token_id: TOKEN_ID_BTC,
816            new_usd_price: 100_000_0000, // $100k USD
817        });
818        client.request_sign_bridge_action(action).await.unwrap();
819    }
820
821    #[tokio::test]
822    async fn test_bridge_server_handle_evm_contract_upgrade_action_path() {
823        let client = setup();
824
825        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
826            nonce: 123,
827            chain_id: BridgeChainId::EthCustom,
828            proxy_address: EthAddress::repeat_byte(6),
829            new_impl_address: EthAddress::repeat_byte(9),
830            call_data: vec![],
831        });
832        client.request_sign_bridge_action(action).await.unwrap();
833
834        let action = BridgeAction::EvmContractUpgradeAction(EvmContractUpgradeAction {
835            nonce: 123,
836            chain_id: BridgeChainId::EthCustom,
837            proxy_address: EthAddress::repeat_byte(6),
838            new_impl_address: EthAddress::repeat_byte(9),
839            call_data: vec![12, 34, 56],
840        });
841        client.request_sign_bridge_action(action).await.unwrap();
842    }
843
844    #[tokio::test]
845    async fn test_bridge_server_handle_add_tokens_on_sui_action_path() {
846        let client = setup();
847
848        let action = BridgeAction::AddTokensOnSuiAction(AddTokensOnSuiAction {
849            nonce: 266,
850            chain_id: BridgeChainId::SuiCustom,
851            native: false,
852            token_ids: vec![100, 101, 102],
853            token_type_names: vec![
854                TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin1").unwrap(),
855                TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin2").unwrap(),
856                TypeTag::from_str("0x0000000000000000000000000000000000000000000000000000000000000abc::my_coin::MyCoin3").unwrap(),
857            ],
858            token_prices: vec![100_000_0000, 200_000_0000, 300_000_0000],
859        });
860        client.request_sign_bridge_action(action).await.unwrap();
861    }
862
863    #[tokio::test]
864    async fn test_bridge_server_handle_add_tokens_on_evm_action_path() {
865        let client = setup();
866
867        let action = BridgeAction::AddTokensOnEvmAction(crate::types::AddTokensOnEvmAction {
868            nonce: 0,
869            chain_id: BridgeChainId::EthCustom,
870            native: false,
871            token_ids: vec![99, 100, 101],
872            token_addresses: vec![
873                EthAddress::repeat_byte(1),
874                EthAddress::repeat_byte(2),
875                EthAddress::repeat_byte(3),
876            ],
877            token_sui_decimals: vec![5, 6, 7],
878            token_prices: vec![1_000_000_000, 2_000_000_000, 3_000_000_000],
879        });
880        client.request_sign_bridge_action(action).await.unwrap();
881    }
882
883    #[tokio::test]
884    async fn test_bridge_server_rejects_oversized_uri() {
885        let mock = BridgeRequestMockHandler::new();
886        let (_handles, ports) = crate::test_utils::run_mock_bridge_server(vec![mock]);
887        let port = ports[0];
888
889        let oversized_query = "a".repeat(MAX_REQUEST_URI_SIZE + 1);
890        let response = reqwest::Client::new()
891            .get(format!("http://127.0.0.1:{port}/ping?{oversized_query}"))
892            .send()
893            .await
894            .unwrap();
895
896        assert_eq!(response.status(), StatusCode::URI_TOO_LONG);
897    }
898
899    #[tokio::test]
900    async fn test_bridge_server_rejects_oversized_body() {
901        let mock = BridgeRequestMockHandler::new();
902        let (_handles, ports) = crate::test_utils::run_mock_bridge_server(vec![mock]);
903        let port = ports[0];
904
905        let oversized_body = "a".repeat(MAX_REQUEST_BODY_SIZE + 1);
906        let response = reqwest::Client::new()
907            .get(format!("http://127.0.0.1:{port}/ping"))
908            .header(CONTENT_LENGTH, oversized_body.len())
909            .body(oversized_body)
910            .send()
911            .await
912            .unwrap();
913
914        assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
915    }
916
917    fn setup() -> BridgeClient {
918        let mock = BridgeRequestMockHandler::new();
919        let (_handles, authorities, mut secrets) =
920            get_test_authorities_and_run_mock_bridge_server(vec![10000], vec![mock.clone()]);
921        mock.set_signer(secrets.swap_remove(0));
922        let committee = BridgeCommittee::new(authorities).unwrap();
923        let pub_key = committee.members().keys().next().unwrap();
924        BridgeClient::new(pub_key.clone(), Arc::new(committee)).unwrap()
925    }
926
927    #[tokio::test]
928    async fn test_bridge_error_response_is_sanitized() {
929        let response = BridgeError::Generic("sensitive server detail".to_string()).into_response();
930        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
931
932        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
933            .await
934            .unwrap();
935        let body = String::from_utf8(body.to_vec()).unwrap();
936        assert_eq!(body, "BridgeError::InternalError");
937        assert!(!body.contains("sensitive server detail"));
938    }
939}