sui_bridge/
crypto.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    error::{BridgeError, BridgeResult},
6    types::{BridgeAction, BridgeCommittee, SignedBridgeAction, VerifiedSignedBridgeAction},
7};
8use alloy::{
9    primitives::Address as EthAddress,
10    signers::k256::{ecdsa::VerifyingKey, elliptic_curve::sec1::ToEncodedPoint},
11};
12use fastcrypto::hash::HashFunction;
13use fastcrypto::{
14    encoding::{Encoding, Hex},
15    error::FastCryptoError,
16    secp256k1::{
17        Secp256k1KeyPair, Secp256k1PublicKey, Secp256k1PublicKeyAsBytes,
18        recoverable::Secp256k1RecoverableSignature,
19    },
20    traits::{RecoverableSigner, ToFromBytes, VerifyRecoverable},
21};
22use fastcrypto::{hash::Keccak256, traits::KeyPair};
23use serde::{Deserialize, Serialize};
24use std::fmt::Debug;
25use std::fmt::{Display, Formatter};
26use sui_types::{base_types::ConciseableName, message_envelope::VerifiedEnvelope};
27use tap::TapFallible;
28pub type BridgeAuthorityKeyPair = Secp256k1KeyPair;
29pub type BridgeAuthorityPublicKey = Secp256k1PublicKey;
30pub type BridgeAuthorityRecoverableSignature = Secp256k1RecoverableSignature;
31
32#[derive(Ord, PartialOrd, PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
33pub struct BridgeAuthorityPublicKeyBytes(Secp256k1PublicKeyAsBytes);
34
35impl BridgeAuthorityPublicKeyBytes {
36    pub fn to_eth_address(&self) -> EthAddress {
37        // unwrap: the conversion should not fail
38        let pubkey = VerifyingKey::from_sec1_bytes(self.as_bytes()).unwrap();
39        let affine: &alloy::signers::k256::AffinePoint = pubkey.as_ref();
40        let encoded = affine.to_encoded_point(false);
41        let pubkey = &encoded.as_bytes()[1..];
42        assert_eq!(pubkey.len(), 64, "raw public key must be 64 bytes");
43        let hash = Keccak256::digest(pubkey).digest;
44        EthAddress::from_slice(&hash[12..])
45    }
46}
47
48impl From<&BridgeAuthorityPublicKey> for BridgeAuthorityPublicKeyBytes {
49    fn from(pk: &BridgeAuthorityPublicKey) -> Self {
50        Self(Secp256k1PublicKeyAsBytes::from(pk))
51    }
52}
53
54impl ToFromBytes for BridgeAuthorityPublicKeyBytes {
55    /// Parse an object from its byte representation
56    fn from_bytes(bytes: &[u8]) -> Result<Self, FastCryptoError> {
57        let pk = BridgeAuthorityPublicKey::from_bytes(bytes)?;
58        Ok(Self::from(&pk))
59    }
60
61    /// Borrow a byte slice representing the serialized form of this object
62    fn as_bytes(&self) -> &[u8] {
63        self.as_ref()
64    }
65}
66
67/// implement `FromStr` for `BridgeAuthorityPublicKeyBytes`
68/// to convert a hex-string to public key bytes.
69impl std::str::FromStr for BridgeAuthorityPublicKeyBytes {
70    type Err = FastCryptoError;
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        let bytes = Hex::decode(s).map_err(|e| {
73            FastCryptoError::GeneralError(format!("Failed to decode hex string: {}", e))
74        })?;
75        Self::from_bytes(&bytes)
76    }
77}
78
79pub struct ConciseBridgeAuthorityPublicKeyBytesRef<'a>(&'a BridgeAuthorityPublicKeyBytes);
80
81impl Debug for ConciseBridgeAuthorityPublicKeyBytesRef<'_> {
82    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
83        let s = Hex::encode(self.0.0.0.get(0..4).ok_or(std::fmt::Error)?);
84        write!(f, "k#{}..", s)
85    }
86}
87
88impl Display for ConciseBridgeAuthorityPublicKeyBytesRef<'_> {
89    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
90        Debug::fmt(self, f)
91    }
92}
93
94impl AsRef<[u8]> for BridgeAuthorityPublicKeyBytes {
95    fn as_ref(&self) -> &[u8] {
96        self.0.0.as_ref()
97    }
98}
99
100impl<'a> ConciseableName<'a> for BridgeAuthorityPublicKeyBytes {
101    type ConciseTypeRef = ConciseBridgeAuthorityPublicKeyBytesRef<'a>;
102    type ConciseType = String;
103
104    fn concise(&'a self) -> ConciseBridgeAuthorityPublicKeyBytesRef<'a> {
105        ConciseBridgeAuthorityPublicKeyBytesRef(self)
106    }
107
108    fn concise_owned(&self) -> String {
109        format!("{:?}", ConciseBridgeAuthorityPublicKeyBytesRef(self))
110    }
111}
112
113// TODO: include epoch ID here to reduce race conditions?
114#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
115pub struct BridgeAuthoritySignInfo {
116    pub authority_pub_key: BridgeAuthorityPublicKey,
117    pub signature: BridgeAuthorityRecoverableSignature,
118}
119
120impl BridgeAuthoritySignInfo {
121    pub fn new(msg: &BridgeAction, secret: &BridgeAuthorityKeyPair) -> Self {
122        let msg_bytes = msg
123            .to_bytes()
124            .expect("Message encoding should not fail for valid actions");
125        Self {
126            authority_pub_key: secret.public().clone(),
127            signature: secret.sign_recoverable_with_hash::<Keccak256>(&msg_bytes),
128        }
129    }
130
131    pub fn verify(&self, msg: &BridgeAction, committee: &BridgeCommittee) -> BridgeResult<()> {
132        // 1. verify committee member is in the committee and not blocklisted
133        if !committee.is_active_member(&self.authority_pub_key_bytes()) {
134            return Err(BridgeError::InvalidBridgeAuthority(
135                self.authority_pub_key_bytes(),
136            ));
137        }
138
139        // 2. verify signature
140        let msg_bytes = msg.to_bytes().map_err(|e| {
141            BridgeError::Generic(format!("Failed to encode message for verification: {}", e))
142        })?;
143
144        self.authority_pub_key
145            .verify_recoverable_with_hash::<Keccak256>(&msg_bytes, &self.signature)
146            .map_err(|e| {
147                BridgeError::InvalidBridgeAuthoritySignature((
148                    self.authority_pub_key_bytes(),
149                    e.to_string(),
150                ))
151            })
152    }
153
154    pub fn authority_pub_key_bytes(&self) -> BridgeAuthorityPublicKeyBytes {
155        BridgeAuthorityPublicKeyBytes::from(&self.authority_pub_key)
156    }
157}
158
159/// Verifies a SignedBridgeAction (response from bridge authority to bridge client)
160/// represents the right BridgeAction, and is signed by the right authority.
161pub fn verify_signed_bridge_action(
162    expected_action: &BridgeAction,
163    signed_action: SignedBridgeAction,
164    expected_signer: &BridgeAuthorityPublicKeyBytes,
165    committee: &BridgeCommittee,
166) -> BridgeResult<VerifiedSignedBridgeAction> {
167    if signed_action.data() != expected_action {
168        return Err(BridgeError::MismatchedAction);
169    }
170
171    let sig = signed_action.auth_sig();
172    if &sig.authority_pub_key_bytes() != expected_signer {
173        return Err(BridgeError::MismatchedAuthoritySigner);
174    }
175    sig.verify(signed_action.data(), committee).tap_err(|e| {
176        tracing::error!(
177            "Failed to verify SignedBridgeEvent {:?}. Error {:?}",
178            signed_action,
179            e
180        )
181    })?;
182    Ok(VerifiedEnvelope::new_from_verified(signed_action))
183}
184
185#[cfg(test)]
186mod tests {
187    use crate::events::EmittedSuiToEthTokenBridgeV1;
188    use crate::test_utils::{get_test_authority_and_key, get_test_sui_to_eth_bridge_action};
189    use crate::types::SignedBridgeAction;
190    use crate::types::{BridgeAction, BridgeAuthority, SuiToEthBridgeAction};
191    use alloy::primitives::Address as EthAddress;
192    use fastcrypto::traits::{KeyPair, ToFromBytes};
193    use prometheus::Registry;
194    use std::str::FromStr;
195    use std::sync::Arc;
196    use sui_types::base_types::SuiAddress;
197    use sui_types::bridge::{BridgeChainId, TOKEN_ID_ETH};
198    use sui_types::crypto::get_key_pair;
199    use sui_types::digests::TransactionDigest;
200
201    use super::*;
202
203    #[test]
204    fn test_sign_and_verify_bridge_event_basic() -> anyhow::Result<()> {
205        telemetry_subscribers::init_for_testing();
206        let registry = Registry::new();
207        mysten_metrics::init_metrics(&registry);
208
209        let (mut authority1, pubkey, secret) = get_test_authority_and_key(5000, 9999);
210        let pubkey_bytes = BridgeAuthorityPublicKeyBytes::from(&pubkey);
211
212        let (authority2, pubkey2, _secret) = get_test_authority_and_key(5000, 9999);
213        let pubkey_bytes2 = BridgeAuthorityPublicKeyBytes::from(&pubkey2);
214
215        let committee = BridgeCommittee::new(vec![authority1.clone(), authority2.clone()]).unwrap();
216
217        let action: BridgeAction =
218            get_test_sui_to_eth_bridge_action(None, Some(1), Some(1), Some(100), None, None, None);
219
220        let sig = BridgeAuthoritySignInfo::new(&action, &secret);
221
222        let signed_action = SignedBridgeAction::new_from_data_and_sig(action.clone(), sig.clone());
223
224        // Verification should succeed
225        let _ =
226            verify_signed_bridge_action(&action, signed_action.clone(), &pubkey_bytes, &committee)
227                .unwrap();
228
229        // Verification should fail - mismatched signer
230        assert!(matches!(
231            verify_signed_bridge_action(&action, signed_action.clone(), &pubkey_bytes2, &committee)
232                .unwrap_err(),
233            BridgeError::MismatchedAuthoritySigner
234        ));
235
236        let mismatched_action: BridgeAction =
237            get_test_sui_to_eth_bridge_action(None, Some(2), Some(3), Some(4), None, None, None);
238        // Verification should fail - mismatched action
239        assert!(matches!(
240            verify_signed_bridge_action(
241                &mismatched_action,
242                signed_action.clone(),
243                &pubkey_bytes2,
244                &committee
245            )
246            .unwrap_err(),
247            BridgeError::MismatchedAction,
248        ));
249
250        // Signature is invalid (signed over different message), verification should fail
251        let action2: BridgeAction =
252            get_test_sui_to_eth_bridge_action(None, Some(3), Some(5), Some(77), None, None, None);
253
254        let invalid_sig = BridgeAuthoritySignInfo::new(&action2, &secret);
255        let signed_action = SignedBridgeAction::new_from_data_and_sig(action.clone(), invalid_sig);
256        let _ = verify_signed_bridge_action(&action, signed_action, &pubkey_bytes, &committee)
257            .unwrap_err();
258
259        // Signer is not in committee, verification should fail
260        let (_, kp2): (_, fastcrypto::secp256k1::Secp256k1KeyPair) = get_key_pair();
261        let pubkey_bytes_2 = BridgeAuthorityPublicKeyBytes::from(kp2.public());
262        let secret2 = Arc::pin(kp2);
263        let sig2 = BridgeAuthoritySignInfo::new(&action, &secret2);
264        let signed_action2 = SignedBridgeAction::new_from_data_and_sig(action.clone(), sig2);
265        let _ = verify_signed_bridge_action(&action, signed_action2, &pubkey_bytes_2, &committee)
266            .unwrap_err();
267
268        // Authority is blocklisted, verification should fail
269        authority1.is_blocklisted = true;
270        let committee = BridgeCommittee::new(vec![authority1, authority2]).unwrap();
271        let signed_action = SignedBridgeAction::new_from_data_and_sig(action.clone(), sig);
272        let _ = verify_signed_bridge_action(&action, signed_action, &pubkey_bytes, &committee)
273            .unwrap_err();
274
275        Ok(())
276    }
277
278    #[test]
279    fn test_bridge_sig_verification_regression_test() {
280        telemetry_subscribers::init_for_testing();
281        let registry = Registry::new();
282        mysten_metrics::init_metrics(&registry);
283
284        let public_key_bytes =
285            Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4")
286                .unwrap();
287        let pubkey1 = BridgeAuthorityPublicKey::from_bytes(&public_key_bytes).unwrap();
288        let authority1 = BridgeAuthority {
289            sui_address: SuiAddress::random_for_testing_only(),
290            pubkey: pubkey1.clone(),
291            voting_power: 2500,
292            is_blocklisted: false,
293            base_url: "".into(),
294        };
295
296        let public_key_bytes =
297            Hex::decode("027f1178ff417fc9f5b8290bd8876f0a157a505a6c52db100a8492203ddd1d4279")
298                .unwrap();
299        let pubkey2 = BridgeAuthorityPublicKey::from_bytes(&public_key_bytes).unwrap();
300        let authority2 = BridgeAuthority {
301            sui_address: SuiAddress::random_for_testing_only(),
302            pubkey: pubkey2.clone(),
303            voting_power: 2500,
304            is_blocklisted: false,
305            base_url: "".into(),
306        };
307
308        let public_key_bytes =
309            Hex::decode("026f311bcd1c2664c14277c7a80e4857c690626597064f89edc33b8f67b99c6bc0")
310                .unwrap();
311        let pubkey3 = BridgeAuthorityPublicKey::from_bytes(&public_key_bytes).unwrap();
312        let authority3 = BridgeAuthority {
313            sui_address: SuiAddress::random_for_testing_only(),
314            pubkey: pubkey3.clone(),
315            voting_power: 2500,
316            is_blocklisted: false,
317            base_url: "".into(),
318        };
319
320        let public_key_bytes =
321            Hex::decode("03a57b85771aedeb6d31c808be9a6e73194e4b70e679608f2bca68bcc684773736")
322                .unwrap();
323        let pubkey4 = BridgeAuthorityPublicKey::from_bytes(&public_key_bytes).unwrap();
324        let authority4 = BridgeAuthority {
325            sui_address: SuiAddress::random_for_testing_only(),
326            pubkey: pubkey4.clone(),
327            voting_power: 2500,
328            is_blocklisted: false,
329            base_url: "".into(),
330        };
331
332        let committee = BridgeCommittee::new(vec![
333            authority1.clone(),
334            authority2.clone(),
335            authority3.clone(),
336            authority4.clone(),
337        ])
338        .unwrap();
339
340        let action = BridgeAction::SuiToEthBridgeAction(SuiToEthBridgeAction {
341            sui_tx_digest: TransactionDigest::random(),
342            sui_tx_event_index: 0,
343            sui_bridge_event: EmittedSuiToEthTokenBridgeV1 {
344                nonce: 1,
345                sui_chain_id: BridgeChainId::SuiTestnet,
346                sui_address: SuiAddress::from_str(
347                    "0x80ab1ee086210a3a37355300ca24672e81062fcdb5ced6618dab203f6a3b291c",
348                )
349                .unwrap(),
350                eth_chain_id: BridgeChainId::EthSepolia,
351                eth_address: EthAddress::from_str("0xb18f79Fe671db47393315fFDB377Da4Ea1B7AF96")
352                    .unwrap(),
353                token_id: TOKEN_ID_ETH,
354                amount_sui_adjusted: 100000u64,
355            },
356        });
357        let sig = BridgeAuthoritySignInfo {
358            authority_pub_key: pubkey1,
359            signature: BridgeAuthorityRecoverableSignature::from_bytes(
360                &Hex::decode("e1cf11b380855ff1d4a451ebc2fd68477cf701b7d4ec88da3082709fe95201a5061b4b60cf13815a80ba9dfead23e220506aa74c4a863ba045d95715b4cc6b6e00").unwrap(),
361            ).unwrap(),
362        };
363        sig.verify(&action, &committee).unwrap();
364
365        let sig = BridgeAuthoritySignInfo {
366            authority_pub_key: pubkey4.clone(),
367            signature: BridgeAuthorityRecoverableSignature::from_bytes(
368                &Hex::decode("8ba9ec92c2d5a44ecc123182f689b901a93921fd35f581354fea20b25a0ded6d055b96a64bdda77dd5a62b93d29abe93640aa3c1a136348093cd7a2418c6bfa301").unwrap(),
369            ).unwrap(),
370        };
371        sig.verify(&action, &committee).unwrap();
372
373        let sig = BridgeAuthoritySignInfo {
374            authority_pub_key: pubkey4,
375            signature: BridgeAuthorityRecoverableSignature::from_bytes(
376                // invalid sdig
377                &Hex::decode("8ba9ec92c2d5a44ecc123182f689b901a93921fd35f581354fea20b25a0ded6d055b96a64bdda77dd5a62b93d29abe93640aa3c1a136348093cd7a2418c6bfa302").unwrap(),
378            ).unwrap(),
379        };
380        sig.verify(&action, &committee).unwrap_err();
381    }
382
383    #[test]
384    fn test_bridge_authority_public_key_bytes_to_eth_address() {
385        let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes(
386            &Hex::decode("02321ede33d2c2d7a8a152f275a1484edef2098f034121a602cb7d767d38680aa4")
387                .unwrap(),
388        )
389        .unwrap();
390        let addr = "0x68b43fd906c0b8f024a18c56e06744f7c6157c65"
391            .parse::<EthAddress>()
392            .unwrap();
393        assert_eq!(pub_key_bytes.to_eth_address(), addr);
394
395        // Example from: https://github.com/gakonst/ethers-rs/blob/master/ethers-core/src/utils/mod.rs#L1235
396        let pub_key_bytes = BridgeAuthorityPublicKeyBytes::from_bytes(
397            &Hex::decode("0376698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762")
398                .unwrap(),
399        )
400        .unwrap();
401        let addr = "0Ac1dF02185025F65202660F8167210A80dD5086"
402            .parse::<EthAddress>()
403            .unwrap();
404        assert_eq!(pub_key_bytes.to_eth_address(), addr);
405    }
406}