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 mut hasher = DefaultHash::default();
149        bcs::serialize_into(&mut hasher, &value).expect("Message serialization should not fail");
150        let digest = hasher.finalize().digest;
151        // Verify each signature against its corresponding signature scheme and public key.
152        // TODO: further optimization can be done because multiple Ed25519 signatures can be batch verified.
153        for (sig, i) in self.sigs.iter().zip_debug_eq(as_indices(self.bitmap)?) {
154            let (subsig_pubkey, weight) =
155                self.multisig_pk
156                    .pk_map
157                    .get(i as usize)
158                    .ok_or(SuiErrorKind::InvalidSignature {
159                        error: "Invalid public keys index".to_string(),
160                    })?;
161            let res = match sig {
162                CompressedSignature::Ed25519(s) => {
163                    if verify_params.additional_multisig_checks
164                        && !matches!(subsig_pubkey.scheme(), SignatureScheme::ED25519)
165                    {
166                        return Err(SuiErrorKind::InvalidSignature {
167                            error: format!(
168                                "Invalid sig for pk={} address={:?} error=signature/pubkey type mismatch",
169                                subsig_pubkey.encode_base64(),
170                                SuiAddress::from(subsig_pubkey)
171                            ),
172                        }.into());
173                    }
174                    let pk =
175                        Ed25519PublicKey::from_bytes(subsig_pubkey.as_ref()).map_err(|_| {
176                            SuiErrorKind::InvalidSignature {
177                                error: "Invalid ed25519 pk bytes".to_string(),
178                            }
179                        })?;
180                    pk.verify(
181                        &digest,
182                        &s.try_into().map_err(|_| SuiErrorKind::InvalidSignature {
183                            error: "Invalid ed25519 signature bytes".to_string(),
184                        })?,
185                    )
186                }
187                CompressedSignature::Secp256k1(s) => {
188                    if verify_params.additional_multisig_checks
189                        && !matches!(subsig_pubkey.scheme(), SignatureScheme::Secp256k1)
190                    {
191                        return Err(SuiErrorKind::InvalidSignature {
192                            error: format!(
193                                "Invalid sig for pk={} address={:?} error=signature/pubkey type mismatch",
194                                subsig_pubkey.encode_base64(),
195                                SuiAddress::from(subsig_pubkey)
196                            ),
197                        }.into());
198                    }
199                    let pk =
200                        Secp256k1PublicKey::from_bytes(subsig_pubkey.as_ref()).map_err(|_| {
201                            SuiErrorKind::InvalidSignature {
202                                error: "Invalid k1 pk bytes".to_string(),
203                            }
204                        })?;
205                    pk.verify(
206                        &digest,
207                        &s.try_into().map_err(|_| SuiErrorKind::InvalidSignature {
208                            error: "Invalid k1 signature bytes".to_string(),
209                        })?,
210                    )
211                }
212                CompressedSignature::Secp256r1(s) => {
213                    if verify_params.additional_multisig_checks
214                        && !matches!(subsig_pubkey.scheme(), SignatureScheme::Secp256r1)
215                    {
216                        return Err(SuiErrorKind::InvalidSignature {
217                            error: format!(
218                                "Invalid sig for pk={} address={:?} error=signature/pubkey type mismatch",
219                                subsig_pubkey.encode_base64(),
220                                SuiAddress::from(subsig_pubkey)
221                            ),
222                        }.into());
223                    }
224                    let pk =
225                        Secp256r1PublicKey::from_bytes(subsig_pubkey.as_ref()).map_err(|_| {
226                            SuiErrorKind::InvalidSignature {
227                                error: "Invalid r1 pk bytes".to_string(),
228                            }
229                        })?;
230                    pk.verify(
231                        &digest,
232                        &s.try_into().map_err(|_| SuiErrorKind::InvalidSignature {
233                            error: "Invalid r1 signature bytes".to_string(),
234                        })?,
235                    )
236                }
237                CompressedSignature::ZkLogin(z) => {
238                    if verify_params.additional_multisig_checks
239                        && !matches!(
240                            subsig_pubkey.scheme(),
241                            SignatureScheme::ZkLoginAuthenticator
242                        )
243                    {
244                        return Err(SuiErrorKind::InvalidSignature {
245                            error: format!(
246                                "Invalid sig for pk={} address={:?} error=signature/pubkey type mismatch",
247                                subsig_pubkey.encode_base64(),
248                                SuiAddress::from(subsig_pubkey)
249                            ),
250                        }.into());
251                    }
252                    let authenticator = ZkLoginAuthenticator::from_bytes(&z.0).map_err(|_| {
253                        SuiErrorKind::InvalidSignature {
254                            error: "Invalid zklogin authenticator bytes".to_string(),
255                        }
256                    })?;
257                    authenticator
258                        .verify_claims(
259                            value,
260                            SuiAddress::from(subsig_pubkey),
261                            verify_params,
262                            zklogin_inputs_cache.clone(),
263                        )
264                        .map_err(|e| FastCryptoError::GeneralError(e.to_string()))
265                }
266                CompressedSignature::Passkey(bytes) => {
267                    let authenticator =
268                        PasskeyAuthenticator::from_bytes(&bytes.0).map_err(|_| {
269                            SuiErrorKind::InvalidSignature {
270                                error: "Invalid passkey authenticator bytes".to_string(),
271                            }
272                        })?;
273                    authenticator
274                        .verify_claims(
275                            value,
276                            SuiAddress::from(subsig_pubkey),
277                            verify_params,
278                            zklogin_inputs_cache.clone(),
279                        )
280                        .map_err(|e| FastCryptoError::GeneralError(e.to_string()))
281                }
282            };
283            if res.is_ok() {
284                weight_sum += *weight as u16;
285            } else {
286                return res.map_err(|e| {
287                    SuiErrorKind::InvalidSignature {
288                        error: format!(
289                            "Invalid sig for pk={} address={:?} error={:?}",
290                            subsig_pubkey.encode_base64(),
291                            SuiAddress::from(subsig_pubkey),
292                            e.to_string()
293                        ),
294                    }
295                    .into()
296                });
297            }
298        }
299        if weight_sum >= self.multisig_pk.threshold {
300            Ok(())
301        } else {
302            Err(SuiErrorKind::InvalidSignature {
303                error: format!(
304                    "Insufficient weight={:?} threshold={:?}",
305                    weight_sum, self.multisig_pk.threshold
306                ),
307            }
308            .into())
309        }
310    }
311}
312
313/// Interpret a bitmap of 01s as a list of indices that is set to 1s.
314/// e.g. 22 = 0b10110, then the result is [1, 2, 4].
315pub fn as_indices(bitmap: u16) -> Result<Vec<u8>, SuiError> {
316    if bitmap > MAX_BITMAP_VALUE {
317        return Err(SuiErrorKind::InvalidSignature {
318            error: "Invalid bitmap".to_string(),
319        }
320        .into());
321    }
322    let mut res = Vec::new();
323    for i in 0..10 {
324        if bitmap & (1 << i) != 0 {
325            res.push(i as u8);
326        }
327    }
328    Ok(res)
329}
330
331impl MultiSig {
332    /// Create MultiSig from its fields without validation
333    pub fn insecure_new(
334        sigs: Vec<CompressedSignature>,
335        bitmap: BitmapUnit,
336        multisig_pk: MultiSigPublicKey,
337    ) -> Self {
338        Self {
339            sigs,
340            bitmap,
341            multisig_pk,
342            bytes: OnceCell::new(),
343        }
344    }
345    /// This combines a list of [enum Signature] `flag || signature || pk` to a MultiSig.
346    /// The order of full_sigs must be the same as the order of public keys in
347    /// [enum MultiSigPublicKey]. e.g. for [pk1, pk2, pk3, pk4, pk5],
348    /// [sig1, sig2, sig5] is valid, but [sig2, sig1, sig5] is invalid.
349    pub fn combine(
350        full_sigs: Vec<GenericSignature>,
351        multisig_pk: MultiSigPublicKey,
352    ) -> Result<Self, SuiError> {
353        multisig_pk
354            .validate()
355            .map_err(|_| SuiErrorKind::InvalidSignature {
356                error: "Invalid multisig public key".to_string(),
357            })?;
358
359        if full_sigs.len() > multisig_pk.pk_map.len() || full_sigs.is_empty() {
360            return Err(SuiErrorKind::InvalidSignature {
361                error: "Invalid number of signatures".to_string(),
362            }
363            .into());
364        }
365        let mut bitmap = 0;
366        let mut sigs = Vec::with_capacity(full_sigs.len());
367        for s in full_sigs {
368            let pk = s.to_public_key()?;
369            let index = multisig_pk
370                .get_index(&pk)
371                .ok_or(SuiErrorKind::IncorrectSigner {
372                    error: format!("pk does not exist: {:?}", pk),
373                })?;
374            if bitmap & (1 << index) != 0 {
375                return Err(SuiErrorKind::InvalidSignature {
376                    error: "Duplicate public key".to_string(),
377                }
378                .into());
379            }
380            bitmap |= 1 << index;
381            sigs.push(s.to_compressed()?);
382        }
383
384        Ok(MultiSig {
385            sigs,
386            bitmap,
387            multisig_pk,
388            bytes: OnceCell::new(),
389        })
390    }
391
392    pub fn init_and_validate(&mut self) -> Result<Self, FastCryptoError> {
393        if self.sigs.len() > self.multisig_pk.pk_map.len()
394            || self.sigs.is_empty()
395            || self.bitmap > MAX_BITMAP_VALUE
396        {
397            return Err(FastCryptoError::InvalidInput);
398        }
399        self.multisig_pk.validate()?;
400        Ok(self.to_owned())
401    }
402
403    pub fn get_pk(&self) -> &MultiSigPublicKey {
404        &self.multisig_pk
405    }
406
407    pub fn get_sigs(&self) -> &[CompressedSignature] {
408        &self.sigs
409    }
410
411    pub fn get_bitmap(&self) -> u16 {
412        self.bitmap
413    }
414
415    pub fn get_zklogin_sigs(&self) -> Result<Vec<ZkLoginAuthenticator>, SuiError> {
416        let authenticator_as_bytes: Vec<_> = self
417            .sigs
418            .iter()
419            .filter_map(|s| match s {
420                CompressedSignature::ZkLogin(z) => Some(z),
421                _ => None,
422            })
423            .collect();
424        authenticator_as_bytes
425            .iter()
426            .map(|z| {
427                ZkLoginAuthenticator::from_bytes(&z.0).map_err(|_| {
428                    SuiErrorKind::InvalidSignature {
429                        error: "Invalid zklogin authenticator bytes".to_string(),
430                    }
431                    .into()
432                })
433            })
434            .collect()
435    }
436
437    pub fn get_indices(&self) -> Result<Vec<u8>, SuiError> {
438        as_indices(self.bitmap)
439    }
440
441    pub fn has_passkey_sigs(&self) -> bool {
442        self.sigs
443            .iter()
444            .any(|s| matches!(s, CompressedSignature::Passkey(_)))
445    }
446}
447
448impl ToFromBytes for MultiSig {
449    fn from_bytes(bytes: &[u8]) -> Result<MultiSig, FastCryptoError> {
450        // The first byte matches the flag of MultiSig.
451        if bytes.first().ok_or(FastCryptoError::InvalidInput)? != &SignatureScheme::MultiSig.flag()
452        {
453            return Err(FastCryptoError::InvalidInput);
454        }
455        let mut multisig: MultiSig =
456            bcs::from_bytes(&bytes[1..]).map_err(|_| FastCryptoError::InvalidSignature)?;
457        multisig.init_and_validate()
458    }
459}
460
461impl FromStr for MultiSig {
462    type Err = SuiError;
463
464    fn from_str(s: &str) -> Result<Self, Self::Err> {
465        let bytes = Base64::decode(s).map_err(|_| SuiErrorKind::InvalidSignature {
466            error: "Invalid base64 string".to_string(),
467        })?;
468        let sig = MultiSig::from_bytes(&bytes).map_err(|_| SuiErrorKind::InvalidSignature {
469            error: "Invalid multisig bytes".to_string(),
470        })?;
471        Ok(sig)
472    }
473}
474
475/// This initialize the underlying bytes representation of MultiSig. It encodes
476/// [struct MultiSig] as the MultiSig flag (0x03) concat with the bcs bytes
477/// of [struct MultiSig] i.e. `flag || bcs_bytes(MultiSig)`.
478impl AsRef<[u8]> for MultiSig {
479    fn as_ref(&self) -> &[u8] {
480        self.bytes
481            .get_or_try_init::<_, eyre::Report>(|| {
482                let as_bytes = bcs::to_bytes(self).expect("BCS serialization should not fail");
483                let mut bytes = Vec::with_capacity(1 + as_bytes.len());
484                bytes.push(SignatureScheme::MultiSig.flag());
485                bytes.extend_from_slice(as_bytes.as_slice());
486                Ok(bytes)
487            })
488            .expect("OnceCell invariant violated")
489    }
490}
491
492/// The struct that contains the public key used for authenticating a MultiSig.
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
494pub struct MultiSigPublicKey {
495    /// A list of public key and its corresponding weight.
496    pk_map: Vec<(PublicKey, WeightUnit)>,
497    /// If the total weight of the public keys corresponding to verified signatures is larger than threshold, the MultiSig is verified.
498    threshold: ThresholdUnit,
499}
500
501impl MultiSigPublicKey {
502    /// Construct MultiSigPublicKey without validation.
503    pub fn insecure_new(pk_map: Vec<(PublicKey, WeightUnit)>, threshold: ThresholdUnit) -> Self {
504        Self { pk_map, threshold }
505    }
506
507    pub fn new(
508        pks: Vec<PublicKey>,
509        weights: Vec<WeightUnit>,
510        threshold: ThresholdUnit,
511    ) -> Result<Self, SuiError> {
512        if pks.is_empty()
513            || weights.is_empty()
514            || threshold == 0
515            || pks.len() != weights.len()
516            || pks.len() > MAX_SIGNER_IN_MULTISIG
517            || weights.contains(&0)
518            || weights
519                .iter()
520                .map(|w| *w as ThresholdUnit)
521                .sum::<ThresholdUnit>()
522                < threshold
523            || pks
524                .iter()
525                .enumerate()
526                .any(|(i, pk)| pks.iter().skip(i + 1).any(|other_pk| *pk == *other_pk))
527        {
528            return Err(SuiErrorKind::InvalidSignature {
529                error: "Invalid multisig public key construction".to_string(),
530            }
531            .into());
532        }
533
534        Ok(MultiSigPublicKey {
535            pk_map: pks.into_iter().zip_debug_eq(weights).collect(),
536            threshold,
537        })
538    }
539
540    pub fn get_index(&self, pk: &PublicKey) -> Option<u8> {
541        self.pk_map.iter().position(|x| &x.0 == pk).map(|x| x as u8)
542    }
543
544    pub fn threshold(&self) -> &ThresholdUnit {
545        &self.threshold
546    }
547
548    pub fn pubkeys(&self) -> &Vec<(PublicKey, WeightUnit)> {
549        &self.pk_map
550    }
551
552    pub fn validate(&self) -> Result<MultiSigPublicKey, FastCryptoError> {
553        let pk_map = self.pubkeys();
554        if self.threshold == 0
555            || pk_map.is_empty()
556            || pk_map.len() > MAX_SIGNER_IN_MULTISIG
557            || pk_map.iter().any(|(_pk, weight)| *weight == 0)
558            || pk_map
559                .iter()
560                .map(|(_pk, weight)| *weight as ThresholdUnit)
561                .sum::<ThresholdUnit>()
562                < self.threshold
563            || pk_map.iter().enumerate().any(|(i, (pk, _weight))| {
564                pk_map
565                    .iter()
566                    .skip(i + 1)
567                    .any(|(other_pk, _weight)| *pk == *other_pk)
568            })
569        {
570            return Err(FastCryptoError::InvalidInput);
571        }
572        Ok(self.to_owned())
573    }
574}