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