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 once_cell::sync::OnceCell;
24use schemars::JsonSchema;
25use serde::{Deserialize, Serialize};
26use serde_with::serde_as;
27use shared_crypto::intent::IntentMessage;
28use std::{
29    hash::{Hash, Hasher},
30    str::FromStr,
31    sync::Arc,
32};
33
34use crate::{
35    base_types::{EpochId, SuiAddress},
36    crypto::PublicKey,
37    error::SuiError,
38};
39
40#[cfg(test)]
41#[path = "unit_tests/multisig_tests.rs"]
42mod multisig_tests;
43
44pub type WeightUnit = u8;
45pub type ThresholdUnit = u16;
46pub type BitmapUnit = u16;
47pub const MAX_SIGNER_IN_MULTISIG: usize = 10;
48pub const MAX_BITMAP_VALUE: BitmapUnit = 0b1111111111;
49/// The struct that contains signatures and public keys necessary for authenticating a MultiSig.
50#[serde_as]
51#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
52pub struct MultiSig {
53    /// The plain signature encoded with signature scheme.
54    sigs: Vec<CompressedSignature>,
55    /// A bitmap that indicates the position of which public key the signature should be authenticated with.
56    bitmap: BitmapUnit,
57    /// The public key encoded with each public key with its signature scheme used along with the corresponding weight.
58    multisig_pk: MultiSigPublicKey,
59    /// A bytes representation of [struct MultiSig]. This helps with implementing [trait AsRef<[u8]>].
60    #[serde(skip)]
61    bytes: OnceCell<Vec<u8>>,
62}
63
64/// Necessary trait for [struct SenderSignedData].
65impl PartialEq for MultiSig {
66    fn eq(&self, other: &Self) -> bool {
67        self.sigs == other.sigs
68            && self.bitmap == other.bitmap
69            && self.multisig_pk == other.multisig_pk
70    }
71}
72
73/// Necessary trait for [struct SenderSignedData].
74impl Eq for MultiSig {}
75
76/// Necessary trait for [struct SenderSignedData].
77impl Hash for MultiSig {
78    fn hash<H: Hasher>(&self, state: &mut H) {
79        self.as_ref().hash(state);
80    }
81}
82
83impl AuthenticatorTrait for MultiSig {
84    fn verify_user_authenticator_epoch(
85        &self,
86        epoch_id: EpochId,
87        max_epoch_upper_bound_delta: Option<u64>,
88    ) -> Result<(), SuiError> {
89        // If there is any zkLogin signatures, filter and check epoch for each.
90        // TODO: call this on all sigs to avoid future lapses
91        self.get_zklogin_sigs()?.iter().try_for_each(|s| {
92            s.verify_user_authenticator_epoch(epoch_id, max_epoch_upper_bound_delta)
93        })
94    }
95
96    fn verify_claims<T>(
97        &self,
98        value: &IntentMessage<T>,
99        multisig_address: SuiAddress,
100        verify_params: &VerifyParams,
101        zklogin_inputs_cache: Arc<VerifiedDigestCache<ZKLoginInputsDigest>>,
102    ) -> Result<(), SuiError>
103    where
104        T: Serialize,
105    {
106        self.multisig_pk
107            .validate()
108            .map_err(|_| SuiErrorKind::InvalidSignature {
109                error: "Invalid multisig pubkey".to_string(),
110            })?;
111
112        // Additional validation on zkLogin public identifier struct if flag is enabled.
113        if verify_params.validate_zklogin_public_identifier {
114            for (pk, _weight) in &self.multisig_pk.pk_map {
115                if let PublicKey::ZkLogin(zklogin_pk) = pk {
116                    zklogin_pk
117                        .validate()
118                        .map_err(|e| SuiErrorKind::InvalidSignature {
119                            error: format!("Invalid zkLogin pk in multisig: {}", e),
120                        })?;
121                }
122            }
123        }
124
125        if SuiAddress::from(&self.multisig_pk) != multisig_address {
126            return Err(SuiErrorKind::InvalidSignature {
127                error: "Invalid address derived from pks".to_string(),
128            }
129            .into());
130        }
131
132        if !self.get_zklogin_sigs()?.is_empty() && !verify_params.accept_zklogin_in_multisig {
133            return Err(SuiErrorKind::InvalidSignature {
134                error: "zkLogin sig not supported inside multisig".to_string(),
135            }
136            .into());
137        }
138
139        if self.has_passkey_sigs() && !verify_params.accept_passkey_in_multisig {
140            return Err(SuiErrorKind::InvalidSignature {
141                error: "Passkey sig not supported inside multisig".to_string(),
142            }
143            .into());
144        }
145
146        let mut weight_sum: u16 = 0;
147        let message = bcs::to_bytes(&value).expect("Message serialization should not fail");
148        let mut hasher = DefaultHash::default();
149        hasher.update(message);
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(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(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}