Skip to main content

sui_types/
multisig.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    crypto::{CompressedSignature, DefaultHash, SignatureScheme},
6    digests::ZKLoginInputsDigest,
7    error::SuiErrorKind,
8    passkey_authenticator::PasskeyAuthenticator,
9    signature::{AuthenticatorTrait, GenericSignature, VerifyParams},
10    signature_verification::VerifiedDigestCache,
11    zk_login_authenticator::ZkLoginAuthenticator,
12};
13pub use enum_dispatch::enum_dispatch;
14use fastcrypto::{
15    ed25519::Ed25519PublicKey,
16    encoding::{Base64, Encoding},
17    error::FastCryptoError,
18    hash::HashFunction,
19    secp256k1::Secp256k1PublicKey,
20    secp256r1::Secp256r1PublicKey,
21    traits::{EncodeDecodeBase64, ToFromBytes, VerifyingKey},
22};
23use fastcrypto_pq::mldsa65::{MLDSA65PublicKey, MLDSA65Signature};
24use mysten_common::ZipDebugEqIteratorExt;
25use once_cell::sync::OnceCell;
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28use serde_with::serde_as;
29use shared_crypto::intent::IntentMessage;
30use std::{
31    hash::{Hash, Hasher},
32    str::FromStr,
33    sync::Arc,
34};
35
36use crate::{
37    base_types::{EpochId, SuiAddress},
38    crypto::PublicKey,
39    error::SuiError,
40};
41
42#[cfg(test)]
43#[path = "unit_tests/multisig_tests.rs"]
44mod multisig_tests;
45
46pub type WeightUnit = u8;
47pub type ThresholdUnit = u16;
48pub type BitmapUnit = u16;
49pub const MAX_SIGNER_IN_MULTISIG: usize = 10;
50pub const MAX_BITMAP_VALUE: BitmapUnit = 0b1111111111;
51/// The struct that contains signatures and public keys necessary for authenticating a MultiSig.
52#[serde_as]
53#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
54pub struct MultiSig {
55    /// The plain signature encoded with signature scheme.
56    sigs: Vec<CompressedSignature>,
57    /// A bitmap that indicates the position of which public key the signature should be authenticated with.
58    bitmap: BitmapUnit,
59    /// The public key encoded with each public key with its signature scheme used along with the corresponding weight.
60    multisig_pk: MultiSigPublicKey,
61    /// A bytes representation of [struct MultiSig]. This helps with implementing [trait AsRef<[u8]>].
62    #[serde(skip)]
63    bytes: OnceCell<Vec<u8>>,
64}
65
66/// Necessary trait for [struct SenderSignedData].
67impl PartialEq for MultiSig {
68    fn eq(&self, other: &Self) -> bool {
69        self.sigs == other.sigs
70            && self.bitmap == other.bitmap
71            && self.multisig_pk == other.multisig_pk
72    }
73}
74
75/// Necessary trait for [struct SenderSignedData].
76impl Eq for MultiSig {}
77
78/// Necessary trait for [struct SenderSignedData].
79impl Hash for MultiSig {
80    fn hash<H: Hasher>(&self, state: &mut H) {
81        self.as_ref().hash(state);
82    }
83}
84
85impl AuthenticatorTrait for MultiSig {
86    fn verify_user_authenticator_epoch(
87        &self,
88        epoch_id: EpochId,
89        max_epoch_upper_bound_delta: Option<u64>,
90    ) -> Result<(), SuiError> {
91        // If there is any zkLogin signatures, filter and check epoch for each.
92        // TODO: call this on all sigs to avoid future lapses
93        self.get_zklogin_sigs()?.iter().try_for_each(|s| {
94            s.verify_user_authenticator_epoch(epoch_id, max_epoch_upper_bound_delta)
95        })
96    }
97
98    fn verify_claims<T>(
99        &self,
100        value: &IntentMessage<T>,
101        multisig_address: SuiAddress,
102        verify_params: &VerifyParams,
103        zklogin_inputs_cache: Arc<VerifiedDigestCache<ZKLoginInputsDigest>>,
104    ) -> Result<(), SuiError>
105    where
106        T: Serialize,
107    {
108        self.multisig_pk
109            .validate()
110            .map_err(|_| SuiErrorKind::InvalidSignature {
111                error: "Invalid multisig pubkey".to_string(),
112            })?;
113
114        // Additional validation on zkLogin public identifier struct if flag is enabled.
115        if verify_params.validate_zklogin_public_identifier {
116            for (pk, _weight) in &self.multisig_pk.pk_map {
117                if let PublicKey::ZkLogin(zklogin_pk) = pk {
118                    zklogin_pk
119                        .validate()
120                        .map_err(|e| SuiErrorKind::InvalidSignature {
121                            error: format!("Invalid zkLogin pk in multisig: {}", e),
122                        })?;
123                }
124            }
125        }
126
127        if SuiAddress::from(&self.multisig_pk) != multisig_address {
128            return Err(SuiErrorKind::InvalidSignature {
129                error: "Invalid address derived from pks".to_string(),
130            }
131            .into());
132        }
133
134        if self.has_zklogin_sigs() && !verify_params.accept_zklogin_in_multisig {
135            return Err(SuiErrorKind::InvalidSignature {
136                error: "zkLogin sig not supported inside multisig".to_string(),
137            }
138            .into());
139        }
140
141        if self.has_passkey_sigs() && !verify_params.accept_passkey_in_multisig {
142            return Err(SuiErrorKind::InvalidSignature {
143                error: "Passkey sig not supported inside multisig".to_string(),
144            }
145            .into());
146        }
147
148        // Gates membership, not just signing: an ML-DSA key in the committee
149        // is a BCS variant that older binaries cannot decode.
150        if self.multisig_pk.has_mldsa65_member() && !verify_params.accept_mldsa65_in_multisig {
151            return Err(SuiErrorKind::InvalidSignature {
152                error: "ML-DSA-65 member not supported inside multisig".to_string(),
153            }
154            .into());
155        }
156
157        let mut weight_sum: u16 = 0;
158        let mut hasher = DefaultHash::default();
159        bcs::serialize_into(&mut hasher, &value).expect("Message serialization should not fail");
160        let digest = hasher.finalize().digest;
161        // Verify each signature against its corresponding signature scheme and public key.
162        // TODO: further optimization can be done because multiple Ed25519 signatures can be batch verified.
163        for (sig, i) in self.sigs.iter().zip_debug_eq(as_indices(self.bitmap)?) {
164            let (subsig_pubkey, weight) =
165                self.multisig_pk
166                    .pk_map
167                    .get(i as usize)
168                    .ok_or(SuiErrorKind::InvalidSignature {
169                        error: "Invalid public keys index".to_string(),
170                    })?;
171            let res = match sig {
172                CompressedSignature::Ed25519(s) => {
173                    if verify_params.additional_multisig_checks
174                        && !matches!(subsig_pubkey.scheme(), SignatureScheme::ED25519)
175                    {
176                        return Err(SuiErrorKind::InvalidSignature {
177                            error: format!(
178                                "Invalid sig for pk={} address={:?} error=signature/pubkey type mismatch",
179                                subsig_pubkey.encode_base64(),
180                                SuiAddress::from(subsig_pubkey)
181                            ),
182                        }.into());
183                    }
184                    let pk =
185                        Ed25519PublicKey::from_bytes(subsig_pubkey.as_ref()).map_err(|_| {
186                            SuiErrorKind::InvalidSignature {
187                                error: "Invalid ed25519 pk bytes".to_string(),
188                            }
189                        })?;
190                    pk.verify(
191                        &digest,
192                        &s.try_into().map_err(|_| SuiErrorKind::InvalidSignature {
193                            error: "Invalid ed25519 signature bytes".to_string(),
194                        })?,
195                    )
196                }
197                CompressedSignature::Secp256k1(s) => {
198                    if verify_params.additional_multisig_checks
199                        && !matches!(subsig_pubkey.scheme(), SignatureScheme::Secp256k1)
200                    {
201                        return Err(SuiErrorKind::InvalidSignature {
202                            error: format!(
203                                "Invalid sig for pk={} address={:?} error=signature/pubkey type mismatch",
204                                subsig_pubkey.encode_base64(),
205                                SuiAddress::from(subsig_pubkey)
206                            ),
207                        }.into());
208                    }
209                    let pk =
210                        Secp256k1PublicKey::from_bytes(subsig_pubkey.as_ref()).map_err(|_| {
211                            SuiErrorKind::InvalidSignature {
212                                error: "Invalid k1 pk bytes".to_string(),
213                            }
214                        })?;
215                    pk.verify(
216                        &digest,
217                        &s.try_into().map_err(|_| SuiErrorKind::InvalidSignature {
218                            error: "Invalid k1 signature bytes".to_string(),
219                        })?,
220                    )
221                }
222                CompressedSignature::Secp256r1(s) => {
223                    if verify_params.additional_multisig_checks
224                        && !matches!(subsig_pubkey.scheme(), SignatureScheme::Secp256r1)
225                    {
226                        return Err(SuiErrorKind::InvalidSignature {
227                            error: format!(
228                                "Invalid sig for pk={} address={:?} error=signature/pubkey type mismatch",
229                                subsig_pubkey.encode_base64(),
230                                SuiAddress::from(subsig_pubkey)
231                            ),
232                        }.into());
233                    }
234                    let pk =
235                        Secp256r1PublicKey::from_bytes(subsig_pubkey.as_ref()).map_err(|_| {
236                            SuiErrorKind::InvalidSignature {
237                                error: "Invalid r1 pk bytes".to_string(),
238                            }
239                        })?;
240                    pk.verify(
241                        &digest,
242                        &s.try_into().map_err(|_| SuiErrorKind::InvalidSignature {
243                            error: "Invalid r1 signature bytes".to_string(),
244                        })?,
245                    )
246                }
247                CompressedSignature::MLDSA65(s) => {
248                    if verify_params.additional_multisig_checks
249                        && !matches!(subsig_pubkey.scheme(), SignatureScheme::MLDSA65)
250                    {
251                        return Err(SuiErrorKind::InvalidSignature {
252                            error: format!(
253                                "Invalid sig for pk={} address={:?} error=signature/pubkey type mismatch",
254                                subsig_pubkey.encode_base64(),
255                                SuiAddress::from(subsig_pubkey)
256                            ),
257                        }.into());
258                    }
259                    let pk =
260                        MLDSA65PublicKey::from_bytes(subsig_pubkey.as_ref()).map_err(|_| {
261                            SuiErrorKind::InvalidSignature {
262                                error: "Invalid mldsa65 pk bytes".to_string(),
263                            }
264                        })?;
265                    pk.verify(
266                        &digest,
267                        &MLDSA65Signature::from_bytes(&s.0).map_err(|_| {
268                            SuiErrorKind::InvalidSignature {
269                                error: "Invalid mldsa65 signature bytes".to_string(),
270                            }
271                        })?,
272                    )
273                }
274                CompressedSignature::ZkLogin(z) => {
275                    if verify_params.additional_multisig_checks
276                        && !matches!(
277                            subsig_pubkey.scheme(),
278                            SignatureScheme::ZkLoginAuthenticator
279                        )
280                    {
281                        return Err(SuiErrorKind::InvalidSignature {
282                            error: format!(
283                                "Invalid sig for pk={} address={:?} error=signature/pubkey type mismatch",
284                                subsig_pubkey.encode_base64(),
285                                SuiAddress::from(subsig_pubkey)
286                            ),
287                        }.into());
288                    }
289                    let authenticator = ZkLoginAuthenticator::from_bytes(&z.0).map_err(|_| {
290                        SuiErrorKind::InvalidSignature {
291                            error: "Invalid zklogin authenticator bytes".to_string(),
292                        }
293                    })?;
294                    authenticator
295                        .verify_claims(
296                            value,
297                            SuiAddress::from(subsig_pubkey),
298                            verify_params,
299                            zklogin_inputs_cache.clone(),
300                        )
301                        .map_err(|e| FastCryptoError::GeneralError(e.to_string()))
302                }
303                CompressedSignature::Passkey(bytes) => {
304                    let authenticator =
305                        PasskeyAuthenticator::from_bytes(&bytes.0).map_err(|_| {
306                            SuiErrorKind::InvalidSignature {
307                                error: "Invalid passkey authenticator bytes".to_string(),
308                            }
309                        })?;
310                    authenticator
311                        .verify_claims(
312                            value,
313                            SuiAddress::from(subsig_pubkey),
314                            verify_params,
315                            zklogin_inputs_cache.clone(),
316                        )
317                        .map_err(|e| FastCryptoError::GeneralError(e.to_string()))
318                }
319            };
320            if res.is_ok() {
321                weight_sum += *weight as u16;
322            } else {
323                return res.map_err(|e| {
324                    SuiErrorKind::InvalidSignature {
325                        error: format!(
326                            "Invalid sig for pk={} address={:?} error={:?}",
327                            subsig_pubkey.encode_base64(),
328                            SuiAddress::from(subsig_pubkey),
329                            e.to_string()
330                        ),
331                    }
332                    .into()
333                });
334            }
335        }
336        if weight_sum >= self.multisig_pk.threshold {
337            Ok(())
338        } else {
339            Err(SuiErrorKind::InvalidSignature {
340                error: format!(
341                    "Insufficient weight={:?} threshold={:?}",
342                    weight_sum, self.multisig_pk.threshold
343                ),
344            }
345            .into())
346        }
347    }
348}
349
350/// Interpret a bitmap of 01s as a list of indices that is set to 1s.
351/// e.g. 22 = 0b10110, then the result is [1, 2, 4].
352pub fn as_indices(bitmap: u16) -> Result<Vec<u8>, SuiError> {
353    if bitmap > MAX_BITMAP_VALUE {
354        return Err(SuiErrorKind::InvalidSignature {
355            error: "Invalid bitmap".to_string(),
356        }
357        .into());
358    }
359    let mut res = Vec::new();
360    for i in 0..10 {
361        if bitmap & (1 << i) != 0 {
362            res.push(i as u8);
363        }
364    }
365    Ok(res)
366}
367
368impl MultiSig {
369    /// Create MultiSig from its fields without validation
370    pub fn insecure_new(
371        sigs: Vec<CompressedSignature>,
372        bitmap: BitmapUnit,
373        multisig_pk: MultiSigPublicKey,
374    ) -> Self {
375        Self {
376            sigs,
377            bitmap,
378            multisig_pk,
379            bytes: OnceCell::new(),
380        }
381    }
382    /// This combines a list of [enum Signature] `flag || signature || pk` to a MultiSig.
383    /// The order of full_sigs must be the same as the order of public keys in
384    /// [enum MultiSigPublicKey]. e.g. for [pk1, pk2, pk3, pk4, pk5],
385    /// [sig1, sig2, sig5] is valid, but [sig2, sig1, sig5] is invalid.
386    pub fn combine(
387        full_sigs: Vec<GenericSignature>,
388        multisig_pk: MultiSigPublicKey,
389    ) -> Result<Self, SuiError> {
390        multisig_pk
391            .validate()
392            .map_err(|_| SuiErrorKind::InvalidSignature {
393                error: "Invalid multisig public key".to_string(),
394            })?;
395
396        if full_sigs.len() > multisig_pk.pk_map.len() || full_sigs.is_empty() {
397            return Err(SuiErrorKind::InvalidSignature {
398                error: "Invalid number of signatures".to_string(),
399            }
400            .into());
401        }
402        let mut bitmap = 0;
403        let mut sigs = Vec::with_capacity(full_sigs.len());
404        for s in full_sigs {
405            let pk = s.to_public_key()?;
406            let index = multisig_pk
407                .get_index(&pk)
408                .ok_or(SuiErrorKind::IncorrectSigner {
409                    error: format!("pk does not exist: {:?}", pk),
410                })?;
411            if bitmap & (1 << index) != 0 {
412                return Err(SuiErrorKind::InvalidSignature {
413                    error: "Duplicate public key".to_string(),
414                }
415                .into());
416            }
417            bitmap |= 1 << index;
418            sigs.push(s.to_compressed()?);
419        }
420
421        Ok(MultiSig {
422            sigs,
423            bitmap,
424            multisig_pk,
425            bytes: OnceCell::new(),
426        })
427    }
428
429    pub fn init_and_validate(&mut self) -> Result<Self, FastCryptoError> {
430        if self.sigs.len() > self.multisig_pk.pk_map.len()
431            || self.sigs.is_empty()
432            || self.bitmap > MAX_BITMAP_VALUE
433        {
434            return Err(FastCryptoError::InvalidInput);
435        }
436        self.multisig_pk.validate()?;
437        Ok(self.to_owned())
438    }
439
440    pub fn get_pk(&self) -> &MultiSigPublicKey {
441        &self.multisig_pk
442    }
443
444    pub fn get_sigs(&self) -> &[CompressedSignature] {
445        &self.sigs
446    }
447
448    pub fn get_bitmap(&self) -> u16 {
449        self.bitmap
450    }
451
452    pub fn get_zklogin_sigs(&self) -> Result<Vec<ZkLoginAuthenticator>, SuiError> {
453        let authenticator_as_bytes: Vec<_> = self
454            .sigs
455            .iter()
456            .filter_map(|s| match s {
457                CompressedSignature::ZkLogin(z) => Some(z),
458                _ => None,
459            })
460            .collect();
461        authenticator_as_bytes
462            .iter()
463            .map(|z| {
464                ZkLoginAuthenticator::from_bytes(&z.0).map_err(|_| {
465                    SuiErrorKind::InvalidSignature {
466                        error: "Invalid zklogin authenticator bytes".to_string(),
467                    }
468                    .into()
469                })
470            })
471            .collect()
472    }
473
474    pub fn get_indices(&self) -> Result<Vec<u8>, SuiError> {
475        as_indices(self.bitmap)
476    }
477
478    pub fn has_passkey_sigs(&self) -> bool {
479        self.sigs
480            .iter()
481            .any(|s| matches!(s, CompressedSignature::Passkey(_)))
482    }
483
484    pub fn has_zklogin_sigs(&self) -> bool {
485        self.sigs
486            .iter()
487            .any(|s| matches!(s, CompressedSignature::ZkLogin(_)))
488    }
489
490    /// Whether any committee key or signature is ML-DSA-65.
491    pub fn uses_mldsa65(&self) -> bool {
492        self.multisig_pk.has_mldsa65_member()
493            || self
494                .sigs
495                .iter()
496                .any(|s| matches!(s, CompressedSignature::MLDSA65(_)))
497    }
498}
499
500impl ToFromBytes for MultiSig {
501    fn from_bytes(bytes: &[u8]) -> Result<MultiSig, FastCryptoError> {
502        // The first byte matches the flag of MultiSig.
503        if bytes.first().ok_or(FastCryptoError::InvalidInput)? != &SignatureScheme::MultiSig.flag()
504        {
505            return Err(FastCryptoError::InvalidInput);
506        }
507        let mut multisig: MultiSig =
508            bcs::from_bytes(&bytes[1..]).map_err(|_| FastCryptoError::InvalidSignature)?;
509        multisig.init_and_validate()
510    }
511}
512
513impl FromStr for MultiSig {
514    type Err = SuiError;
515
516    fn from_str(s: &str) -> Result<Self, Self::Err> {
517        let bytes = Base64::decode(s).map_err(|_| SuiErrorKind::InvalidSignature {
518            error: "Invalid base64 string".to_string(),
519        })?;
520        let sig = MultiSig::from_bytes(&bytes).map_err(|_| SuiErrorKind::InvalidSignature {
521            error: "Invalid multisig bytes".to_string(),
522        })?;
523        Ok(sig)
524    }
525}
526
527/// This initialize the underlying bytes representation of MultiSig. It encodes
528/// [struct MultiSig] as the MultiSig flag (0x03) concat with the bcs bytes
529/// of [struct MultiSig] i.e. `flag || bcs_bytes(MultiSig)`.
530impl AsRef<[u8]> for MultiSig {
531    fn as_ref(&self) -> &[u8] {
532        self.bytes
533            .get_or_try_init::<_, eyre::Report>(|| {
534                let as_bytes = bcs::to_bytes(self).expect("BCS serialization should not fail");
535                let mut bytes = Vec::with_capacity(1 + as_bytes.len());
536                bytes.push(SignatureScheme::MultiSig.flag());
537                bytes.extend_from_slice(as_bytes.as_slice());
538                Ok(bytes)
539            })
540            .expect("OnceCell invariant violated")
541    }
542}
543
544/// The struct that contains the public key used for authenticating a MultiSig.
545#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
546pub struct MultiSigPublicKey {
547    /// A list of public key and its corresponding weight.
548    pk_map: Vec<(PublicKey, WeightUnit)>,
549    /// If the total weight of the public keys corresponding to verified signatures is larger than threshold, the MultiSig is verified.
550    threshold: ThresholdUnit,
551}
552
553impl MultiSigPublicKey {
554    /// Construct MultiSigPublicKey without validation.
555    pub fn insecure_new(pk_map: Vec<(PublicKey, WeightUnit)>, threshold: ThresholdUnit) -> Self {
556        Self { pk_map, threshold }
557    }
558
559    pub fn new(
560        pks: Vec<PublicKey>,
561        weights: Vec<WeightUnit>,
562        threshold: ThresholdUnit,
563    ) -> Result<Self, SuiError> {
564        if pks.is_empty()
565            || weights.is_empty()
566            || threshold == 0
567            || pks.len() != weights.len()
568            || pks.len() > MAX_SIGNER_IN_MULTISIG
569            || weights.contains(&0)
570            || weights
571                .iter()
572                .map(|w| *w as ThresholdUnit)
573                .sum::<ThresholdUnit>()
574                < threshold
575            || pks
576                .iter()
577                .enumerate()
578                .any(|(i, pk)| pks.iter().skip(i + 1).any(|other_pk| *pk == *other_pk))
579        {
580            return Err(SuiErrorKind::InvalidSignature {
581                error: "Invalid multisig public key construction".to_string(),
582            }
583            .into());
584        }
585
586        Ok(MultiSigPublicKey {
587            pk_map: pks.into_iter().zip_debug_eq(weights).collect(),
588            threshold,
589        })
590    }
591
592    pub fn get_index(&self, pk: &PublicKey) -> Option<u8> {
593        self.pk_map.iter().position(|x| &x.0 == pk).map(|x| x as u8)
594    }
595
596    pub fn threshold(&self) -> &ThresholdUnit {
597        &self.threshold
598    }
599
600    pub fn pubkeys(&self) -> &Vec<(PublicKey, WeightUnit)> {
601        &self.pk_map
602    }
603
604    pub fn has_mldsa65_member(&self) -> bool {
605        self.pk_map
606            .iter()
607            .any(|(pk, _)| matches!(pk, PublicKey::MLDSA65(_)))
608    }
609
610    pub fn validate(&self) -> Result<MultiSigPublicKey, FastCryptoError> {
611        let pk_map = self.pubkeys();
612        if self.threshold == 0
613            || pk_map.is_empty()
614            || pk_map.len() > MAX_SIGNER_IN_MULTISIG
615            || pk_map.iter().any(|(_pk, weight)| *weight == 0)
616            || pk_map
617                .iter()
618                .map(|(_pk, weight)| *weight as ThresholdUnit)
619                .sum::<ThresholdUnit>()
620                < self.threshold
621            || pk_map.iter().enumerate().any(|(i, (pk, _weight))| {
622                pk_map
623                    .iter()
624                    .skip(i + 1)
625                    .any(|(other_pk, _weight)| *pk == *other_pk)
626            })
627        {
628            return Err(FastCryptoError::InvalidInput);
629        }
630        Ok(self.to_owned())
631    }
632}