1use 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#[serde_as]
53#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
54pub struct MultiSig {
55 sigs: Vec<CompressedSignature>,
57 bitmap: BitmapUnit,
59 multisig_pk: MultiSigPublicKey,
61 #[serde(skip)]
63 bytes: OnceCell<Vec<u8>>,
64}
65
66impl 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
75impl Eq for MultiSig {}
77
78impl 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 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 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 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 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
350pub 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 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 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 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 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
527impl 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
546pub struct MultiSigPublicKey {
547 pk_map: Vec<(PublicKey, WeightUnit)>,
549 threshold: ThresholdUnit,
551}
552
553impl MultiSigPublicKey {
554 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}