1#[cfg(feature = "serde")]
6use super::SignatureFromBytesError;
7use super::{
8 Ed25519Signature, PublicKey, Secp256k1Signature, Secp256r1Signature, SignatureScheme,
9 SimpleSignature, passkey::PasskeyAuthenticator,
10};
11use crate::UserSignature;
12
13pub type WeightUnit = u8;
14pub type ThresholdUnit = u16;
15pub type BitmapUnit = u16;
16
17pub const MULTISIG_COMMITTEE_SIZE_MAX: usize = 10;
18pub const MULTISIG_BITMAP_VALUE_MAX: BitmapUnit = 0b1111111111;
19
20#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum MultisigError {
23 #[error("{0}")]
24 TryFromSlice(#[from] std::array::TryFromSliceError),
25 #[error("{0}")]
26 Base64(#[from] base64ct::Error),
27 #[cfg(feature = "serde")]
28 #[error("{0}")]
29 SignatureFromBytes(#[from] SignatureFromBytesError),
30 #[error("Multisig threshold must be non-zero")]
31 ZeroThreshold,
32 #[error("Multisig committee must have at least one member")]
33 EmptyCommittee,
34 #[error(
35 "Multisig committee size {0} exceeds maximum size of {MULTISIG_COMMITTEE_SIZE_MAX} members"
36 )]
37 CommitteeTooLarge(usize),
38 #[error("Multisig committee contains a member with zero weight")]
39 ZeroWeightMember,
40 #[error("Insufficient total weight {0} for threshold {1}")]
41 InsufficientWeight(ThresholdUnit, ThresholdUnit),
42 #[error("UnallowedSignatureType")]
43 UnallowedSignatureType,
44 #[error("Invalid input")]
45 InvalidInput,
46 #[error("Duplicate public key")]
47 DuplicatePublicKey,
48 #[error("Signatures are not in committee order")]
49 SignaturesOutOfOrder,
50 #[error("No public key found for signature at index: {0}")]
51 NoPublicKeyForSignature(usize),
52 #[error("Invalid number of signatures")]
53 InvalidSignatureNumber,
54 #[error("Invalid bitmap value: {0}")]
55 InvalidBitmap(u16),
56}
57
58#[derive(Clone, Debug, Eq, Hash, PartialEq)]
76#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
77#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
78#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
79pub struct MultisigMember {
80 public_key: PublicKey,
81 #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "u8"))]
82 weight: WeightUnit,
83}
84
85impl MultisigMember {
86 pub fn new(public_key: impl Into<PublicKey>, weight: WeightUnit) -> Self {
88 Self {
89 public_key: public_key.into(),
90 weight,
91 }
92 }
93
94 pub fn public_key(&self) -> &PublicKey {
96 &self.public_key
97 }
98
99 pub fn weight(&self) -> WeightUnit {
101 self.weight
102 }
103}
104
105impl crate::TreeDisplay for MultisigMember {
106 fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
107 w.header("Multisig Member")?;
108 w.leaf("Public Key", &self.public_key, false)?;
109 w.leaf("Weight", &self.weight, true)
110 }
111}
112
113#[derive(Clone, Debug, Eq, Hash, PartialEq)]
136#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
137#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
138pub struct MultisigCommittee {
139 members: Vec<MultisigMember>,
141 #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "u16"))]
144 threshold: ThresholdUnit,
145}
146
147impl MultisigCommittee {
148 pub fn new_unchecked(members: Vec<MultisigMember>, threshold: ThresholdUnit) -> Self {
163 Self { members, threshold }
164 }
165
166 pub fn new(
180 members: Vec<MultisigMember>,
181 threshold: ThresholdUnit,
182 ) -> Result<Self, MultisigError> {
183 let committee = Self::new_unchecked(members, threshold);
184
185 committee.validate()?;
186
187 Ok(committee)
188 }
189
190 pub fn members(&self) -> &[MultisigMember] {
192 &self.members
193 }
194
195 pub fn threshold(&self) -> ThresholdUnit {
198 self.threshold
199 }
200
201 pub fn scheme(&self) -> SignatureScheme {
203 SignatureScheme::Multisig
204 }
205
206 pub fn get_public_key_index(&self, public_key: &PublicKey) -> Option<u8> {
208 self.members
209 .iter()
210 .position(|member| &member.public_key == public_key)
211 .map(|x| x as u8)
212 }
213
214 pub fn validate(&self) -> Result<(), MultisigError> {
224 if self.threshold == 0 {
225 return Err(MultisigError::ZeroThreshold);
226 }
227 if self.members.is_empty() {
228 return Err(MultisigError::EmptyCommittee);
229 }
230 if self.members.len() > MULTISIG_COMMITTEE_SIZE_MAX {
231 return Err(MultisigError::CommitteeTooLarge(self.members.len()));
232 }
233 if self.members.iter().any(|member| member.weight == 0) {
234 return Err(MultisigError::ZeroWeightMember);
235 }
236 let total_weight: ThresholdUnit = self
237 .members
238 .iter()
239 .map(|member| member.weight as ThresholdUnit)
240 .sum();
241 if total_weight < self.threshold {
242 return Err(MultisigError::InsufficientWeight(
243 total_weight,
244 self.threshold,
245 ));
246 }
247 for (i, member) in self.members.iter().enumerate() {
248 if self
249 .members
250 .iter()
251 .skip(i + 1)
252 .any(|m| m.public_key == member.public_key)
253 {
254 return Err(MultisigError::DuplicatePublicKey);
255 }
256 }
257
258 Ok(())
259 }
260}
261
262impl crate::TreeDisplay for MultisigCommittee {
263 fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
264 w.header("Multisig Committee")?;
265 w.children("Members", &self.members, false)?;
266 w.leaf("Threshold", &self.threshold, true)
267 }
268}
269
270#[derive(Clone, Debug, Eq, Hash, PartialEq)]
296#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
297pub struct MultisigAggregatedSignature {
298 signatures: Vec<MultisigMemberSignature>,
303 #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "u16"))]
306 bitmap: BitmapUnit,
307 committee: MultisigCommittee,
310}
311
312impl MultisigAggregatedSignature {
313 pub fn new_unchecked(
333 signatures: Vec<MultisigMemberSignature>,
334 bitmap: BitmapUnit,
335 committee: MultisigCommittee,
336 ) -> Self {
337 Self {
338 signatures,
339 bitmap,
340 committee,
341 }
342 }
343
344 pub fn new(
360 signatures: Vec<UserSignature>,
361 committee: MultisigCommittee,
362 ) -> Result<Self, MultisigError> {
363 if signatures.len() > committee.members.len() || signatures.is_empty() {
364 return Err(MultisigError::InvalidSignatureNumber);
365 }
366
367 committee.validate()?;
368
369 let mut bitmap = 0;
370 let mut member_signatures = Vec::with_capacity(signatures.len());
371 let mut prev_index: Option<u8> = None;
372 for (sig_index, signature) in signatures.into_iter().enumerate() {
373 let pk = signature
374 .to_public_key()
375 .map_err(|_| MultisigError::UnallowedSignatureType)?;
376 let index = committee
377 .get_public_key_index(&pk)
378 .ok_or(MultisigError::NoPublicKeyForSignature(sig_index))?;
379 if bitmap & (1 << index) != 0 {
380 return Err(MultisigError::DuplicatePublicKey);
381 }
382 if let Some(prev) = prev_index
383 && index < prev
384 {
385 return Err(MultisigError::SignaturesOutOfOrder);
386 }
387 bitmap |= 1 << index;
388 prev_index = Some(index);
389 member_signatures.push(signature.try_into()?);
390 }
391
392 let signature = MultisigAggregatedSignature {
393 signatures: member_signatures,
394 bitmap,
395 committee,
396 };
397
398 Ok(signature)
399 }
400
401 pub fn validate(&self) -> Result<(), MultisigError> {
403 self.committee.validate()?;
404
405 if self.signatures.len() > self.committee.members.len() || self.signatures.is_empty() {
406 return Err(MultisigError::InvalidSignatureNumber);
407 }
408
409 let bits_past_committee = self
410 .bitmap
411 .checked_shr(self.committee.members.len() as u32)
412 .unwrap_or(0);
413 if bits_past_committee != 0 || self.signatures.len() != self.bitmap.count_ones() as usize {
414 return Err(MultisigError::InvalidBitmap(self.bitmap));
415 }
416
417 Ok(())
418 }
419
420 pub fn signatures(&self) -> &[MultisigMemberSignature] {
422 &self.signatures
423 }
424
425 pub fn bitmap(&self) -> BitmapUnit {
428 self.bitmap
429 }
430
431 pub fn indices(&self) -> Result<Vec<u8>, MultisigError> {
438 as_indices(self.bitmap)
439 }
440
441 pub fn committee(&self) -> &MultisigCommittee {
443 &self.committee
444 }
445
446 pub fn contains_signature_scheme(&self, scheme: SignatureScheme) -> bool {
449 self.signatures.iter().any(|s| s.scheme() == scheme)
450 }
451}
452
453impl crate::TreeDisplay for MultisigAggregatedSignature {
454 fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
455 w.header("Multisig Aggregated Signature")?;
456 w.child("Committee", &self.committee, false)?;
457 w.children("Signatures", &self.signatures, false)?;
458 w.leaf("Bitmap", &self.bitmap, true)
459 }
460}
461
462crate::impl_tree_display!(
463 MultisigMember,
464 MultisigCommittee,
465 MultisigAggregatedSignature,
466 MultisigMemberSignature
467);
468
469fn as_indices(bitmap: u16) -> Result<Vec<u8>, MultisigError> {
472 if bitmap > MULTISIG_BITMAP_VALUE_MAX {
473 return Err(MultisigError::InvalidBitmap(bitmap));
474 }
475 let mut res = Vec::new();
476 for i in 0..MULTISIG_COMMITTEE_SIZE_MAX {
477 if bitmap & (1 << i) != 0 {
478 res.push(i as u8);
479 }
480 }
481 Ok(res)
482}
483
484#[derive(Clone, Debug, derive_more::From, Eq, Hash, PartialEq)]
502#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
503#[non_exhaustive]
504pub enum MultisigMemberSignature {
505 Ed25519(Ed25519Signature),
506 Secp256k1(Secp256k1Signature),
507 Secp256r1(Secp256r1Signature),
508 Passkey(PasskeyAuthenticator),
509}
510
511impl crate::TreeDisplay for MultisigMemberSignature {
512 fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
513 w.enum_name("Multisig Member Signature");
514 match self {
515 Self::Ed25519(v) => w.header(&format!("Ed25519Signature({v})")),
516 Self::Secp256k1(v) => w.header(&format!("Secp256k1Signature({v})")),
517 Self::Secp256r1(v) => w.header(&format!("Secp256r1Signature({v})")),
518 Self::Passkey(v) => v.fmt_tree(w),
519 }
520 }
521}
522
523impl MultisigMemberSignature {
524 crate::def_is_as_into_opt!(
525 Ed25519(Ed25519Signature),
526 Secp256k1(Secp256k1Signature),
527 Secp256r1(Secp256r1Signature),
528 Passkey(PasskeyAuthenticator),
529 );
530
531 pub fn scheme(&self) -> SignatureScheme {
532 match self {
533 Self::Ed25519(_) => SignatureScheme::Ed25519,
534 Self::Secp256k1(_) => SignatureScheme::Secp256k1,
535 Self::Secp256r1(_) => SignatureScheme::Secp256r1,
536 Self::Passkey(_) => SignatureScheme::PasskeyAuthenticator,
537 }
538 }
539}
540
541impl AsRef<[u8]> for MultisigMemberSignature {
542 fn as_ref(&self) -> &[u8] {
543 match self {
544 Self::Ed25519(s) => s.as_ref(),
545 Self::Secp256k1(s) => s.as_ref(),
546 Self::Secp256r1(s) => s.as_ref(),
547 Self::Passkey(s) => s.signature.as_ref(),
548 }
549 }
550}
551
552impl TryFrom<UserSignature> for MultisigMemberSignature {
553 type Error = MultisigError;
554
555 fn try_from(signature: UserSignature) -> Result<Self, Self::Error> {
556 match signature {
557 UserSignature::Simple(simple) => Ok(simple.into()),
558 UserSignature::Multisig(_) => Err(MultisigError::UnallowedSignatureType),
559 UserSignature::PasskeyAuthenticator(auth) => Ok(Self::Passkey(auth)),
560 UserSignature::MoveAuthenticator(_) => Err(MultisigError::UnallowedSignatureType),
561 }
562 }
563}
564
565impl From<SimpleSignature> for MultisigMemberSignature {
566 fn from(signature: SimpleSignature) -> Self {
567 match signature {
568 SimpleSignature::Ed25519 { signature, .. } => Self::Ed25519(signature),
569 SimpleSignature::Secp256k1 { signature, .. } => Self::Secp256k1(signature),
570 SimpleSignature::Secp256r1 { signature, .. } => Self::Secp256r1(signature),
571 }
572 }
573}
574
575#[cfg(feature = "proptest")]
576impl proptest::arbitrary::Arbitrary for MultisigCommittee {
577 type Parameters = ();
578 type Strategy = proptest::strategy::BoxedStrategy<Self>;
579
580 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
581 use proptest::{collection::vec, prelude::*};
582
583 vec(
584 (any::<PublicKey>(), 1u8..=WeightUnit::MAX),
585 1..=MULTISIG_COMMITTEE_SIZE_MAX,
586 )
587 .prop_flat_map(|raw_members| {
588 let mut members: Vec<MultisigMember> = Vec::with_capacity(raw_members.len());
589 for (public_key, weight) in raw_members {
590 if !members.iter().any(|m| m.public_key == public_key) {
591 members.push(MultisigMember { public_key, weight });
592 }
593 }
594 let sum: ThresholdUnit = members.iter().map(|m| m.weight as ThresholdUnit).sum();
595 (Just(members), 1..=sum)
596 })
597 .prop_map(|(members, threshold)| Self { members, threshold })
598 .boxed()
599 }
600}
601
602#[cfg(feature = "proptest")]
603impl proptest::arbitrary::Arbitrary for MultisigAggregatedSignature {
604 type Parameters = ();
605 type Strategy = proptest::strategy::BoxedStrategy<Self>;
606
607 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
608 use proptest::{collection::vec, prelude::*, sample::subsequence};
609
610 any::<MultisigCommittee>()
611 .prop_flat_map(|committee| {
612 let n = committee.members.len();
613 let all_indices: Vec<usize> = (0..n).collect();
614 (Just(committee), subsequence(all_indices, 1..=n))
615 })
616 .prop_flat_map(|(committee, indices)| {
617 let count = indices.len();
618 let bitmap = indices
619 .iter()
620 .fold(0 as BitmapUnit, |acc, &i| acc | (1 << i));
621 (
622 Just(committee),
623 Just(bitmap),
624 vec(any::<MultisigMemberSignature>(), count..=count),
625 )
626 })
627 .prop_map(|(committee, bitmap, signatures)| Self {
628 signatures,
629 bitmap,
630 committee,
631 })
632 .boxed()
633 }
634}
635
636#[cfg(feature = "serde")]
637#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
638pub(crate) mod serialization {
639 use std::{borrow::Cow, str::FromStr};
640
641 use base64ct::{Base64, Encoding};
642 use serde::{Deserialize, Deserializer, Serialize, Serializer};
643 use serde_with::{Bytes, DeserializeAs};
644
645 use super::*;
646 use crate::{SignatureScheme, crypto::SignatureFromBytesError};
647
648 #[derive(serde::Deserialize)]
655 pub(crate) struct Multisig {
656 signatures: Vec<MultisigMemberSignature>,
657 bitmap: BitmapUnit,
658 committee: MultisigCommittee,
659 }
660
661 #[derive(serde::Serialize)]
662 struct MultisigRef<'a> {
663 signatures: &'a [MultisigMemberSignature],
664 bitmap: BitmapUnit,
665 committee: &'a MultisigCommittee,
666 }
667
668 impl TryFrom<Multisig> for MultisigAggregatedSignature {
669 type Error = SignatureFromBytesError;
670
671 fn try_from(multisig: Multisig) -> Result<Self, Self::Error> {
672 let multisig = Self {
673 signatures: multisig.signatures,
674 bitmap: multisig.bitmap,
675 committee: multisig.committee,
676 };
677 multisig
678 .validate()
679 .map_err(|e| SignatureFromBytesError::new(format!("invalid multisig: {e}")))?;
680 Ok(multisig)
681 }
682 }
683
684 #[derive(serde::Deserialize)]
685 #[serde(rename = "MultisigAggregatedSignature")]
686 struct ReadableMultisigAggregatedSignature {
687 signatures: Vec<MultisigMemberSignature>,
688 bitmap: BitmapUnit,
689 committee: MultisigCommittee,
690 }
691
692 #[derive(serde::Serialize)]
693 #[serde(rename = "MultisigAggregatedSignature")]
694 struct ReadableMultisigAggregatedSignatureRef<'a> {
695 signatures: &'a [MultisigMemberSignature],
696 bitmap: BitmapUnit,
697 committee: &'a MultisigCommittee,
698 }
699
700 impl Serialize for MultisigAggregatedSignature {
701 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
702 where
703 S: Serializer,
704 {
705 if serializer.is_human_readable() {
706 let readable = ReadableMultisigAggregatedSignatureRef {
707 signatures: &self.signatures,
708 bitmap: self.bitmap,
709 committee: &self.committee,
710 };
711 readable.serialize(serializer)
712 } else {
713 let bytes = self.to_bytes();
714 serializer.serialize_bytes(&bytes)
715 }
716 }
717 }
718
719 impl<'de> Deserialize<'de> for MultisigAggregatedSignature {
720 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
721 where
722 D: Deserializer<'de>,
723 {
724 if deserializer.is_human_readable() {
725 let readable = ReadableMultisigAggregatedSignature::deserialize(deserializer)?;
726 Ok(Self {
727 signatures: readable.signatures,
728 bitmap: readable.bitmap,
729 committee: readable.committee,
730 })
731 } else {
732 let bytes: Cow<'de, [u8]> = Bytes::deserialize_as(deserializer)?;
733 Self::from_bytes(bytes).map_err(serde::de::Error::custom)
734 }
735 }
736 }
737
738 impl MultisigAggregatedSignature {
739 pub fn to_bytes(&self) -> Vec<u8> {
742 let mut buf = vec![SignatureScheme::Multisig as u8];
743 let multisig = MultisigRef {
744 signatures: &self.signatures,
745 bitmap: self.bitmap,
746 committee: &self.committee,
747 };
748 bcs::serialize_into(&mut buf, &multisig).expect("serialization cannot fail");
749 buf
750 }
751
752 pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, SignatureFromBytesError> {
753 let bytes = bytes.as_ref();
754 let (flag, tail) = bytes.split_first().ok_or(SignatureFromBytesError::new(
755 "missing signature scheme flag",
756 ))?;
757 let scheme = SignatureScheme::from_byte(*flag).map_err(SignatureFromBytesError::new)?;
758
759 if scheme != SignatureScheme::Multisig {
760 return Err(SignatureFromBytesError::new("invalid multisig flag"));
761 }
762
763 bcs::from_bytes::<Multisig>(tail)
764 .map_err(|_| SignatureFromBytesError::new("invalid multisig"))?
765 .try_into()
766 }
767 }
768
769 #[derive(serde::Deserialize, serde::Serialize)]
776 #[cfg_attr(
777 feature = "bcs-schema",
778 derive(iota_bcs_schema::BcsSchema),
779 bcs_schema(name = "multisig-member-signature")
780 )]
781 enum MemberSignature {
782 Ed25519(Ed25519Signature),
783 Secp256k1(Secp256k1Signature),
784 Secp256r1(Secp256r1Signature),
785 #[cfg_attr(feature = "bcs-schema", bcs_schema(skip))]
786 ZkLoginDeprecated,
787 Passkey(
788 #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "bytes"))] PasskeyAuthenticator,
789 ),
790 }
791
792 #[derive(serde::Deserialize, serde::Serialize)]
793 #[serde(tag = "scheme", rename_all = "lowercase")]
794 #[serde(rename = "MultisigMemberSignature")]
795 enum ReadableMemberSignature {
796 Ed25519 { signature: Ed25519Signature },
797 Secp256k1 { signature: Secp256k1Signature },
798 Secp256r1 { signature: Secp256r1Signature },
799 ZkLoginDeprecated,
800 Passkey(PasskeyAuthenticator),
801 }
802
803 impl Serialize for MultisigMemberSignature {
804 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
805 where
806 S: Serializer,
807 {
808 if serializer.is_human_readable() {
809 let readable = match self {
810 MultisigMemberSignature::Ed25519(signature) => {
811 ReadableMemberSignature::Ed25519 {
812 signature: *signature,
813 }
814 }
815 MultisigMemberSignature::Secp256k1(signature) => {
816 ReadableMemberSignature::Secp256k1 {
817 signature: *signature,
818 }
819 }
820 MultisigMemberSignature::Secp256r1(signature) => {
821 ReadableMemberSignature::Secp256r1 {
822 signature: *signature,
823 }
824 }
825 MultisigMemberSignature::Passkey(authenticator) => {
826 ReadableMemberSignature::Passkey(authenticator.clone())
827 }
828 };
829 readable.serialize(serializer)
830 } else {
831 let binary = match self {
832 MultisigMemberSignature::Ed25519(signature) => {
833 MemberSignature::Ed25519(*signature)
834 }
835 MultisigMemberSignature::Secp256k1(signature) => {
836 MemberSignature::Secp256k1(*signature)
837 }
838 MultisigMemberSignature::Secp256r1(signature) => {
839 MemberSignature::Secp256r1(*signature)
840 }
841 MultisigMemberSignature::Passkey(authenticator) => {
842 MemberSignature::Passkey(authenticator.clone())
843 }
844 };
845 binary.serialize(serializer)
846 }
847 }
848 }
849
850 impl<'de> Deserialize<'de> for MultisigMemberSignature {
851 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
852 where
853 D: Deserializer<'de>,
854 {
855 if deserializer.is_human_readable() {
856 let readable = ReadableMemberSignature::deserialize(deserializer)?;
857 Ok(match readable {
858 ReadableMemberSignature::Ed25519 { signature } => Self::Ed25519(signature),
859 ReadableMemberSignature::Secp256k1 { signature } => Self::Secp256k1(signature),
860 ReadableMemberSignature::Secp256r1 { signature } => Self::Secp256r1(signature),
861 ReadableMemberSignature::ZkLoginDeprecated => {
862 return Err(serde::de::Error::custom(
863 "zkLoginDeprecated is not supported",
864 ));
865 }
866 ReadableMemberSignature::Passkey(authenticator) => Self::Passkey(authenticator),
867 })
868 } else {
869 let binary = MemberSignature::deserialize(deserializer)?;
870 Ok(match binary {
871 MemberSignature::Ed25519(signature) => Self::Ed25519(signature),
872 MemberSignature::Secp256k1(signature) => Self::Secp256k1(signature),
873 MemberSignature::Secp256r1(signature) => Self::Secp256r1(signature),
874 MemberSignature::ZkLoginDeprecated => {
875 return Err(serde::de::Error::custom(
876 "zkLoginDeprecated is not supported",
877 ));
878 }
879 MemberSignature::Passkey(authenticator) => Self::Passkey(authenticator),
880 })
881 }
882 }
883 }
884
885 impl FromStr for MultisigAggregatedSignature {
886 type Err = MultisigError;
887
888 fn from_str(s: &str) -> Result<Self, Self::Err> {
889 let bytes = Base64::decode_vec(s)?;
890 let sig = MultisigAggregatedSignature::from_bytes(&bytes)?;
891
892 Ok(sig)
893 }
894 }
895
896 impl MultisigMemberSignature {
897 pub fn to_base64(&self) -> String {
898 let mut bytes: Vec<u8> = Vec::new();
899
900 match self {
901 MultisigMemberSignature::Ed25519(signature) => {
902 bytes.extend_from_slice(&[self.scheme() as u8]);
903 bytes.extend_from_slice(signature.as_ref());
904 }
905 MultisigMemberSignature::Secp256k1(signature) => {
906 bytes.extend_from_slice(&[self.scheme() as u8]);
907 bytes.extend_from_slice(signature.as_ref());
908 }
909 MultisigMemberSignature::Secp256r1(signature) => {
910 bytes.extend_from_slice(&[self.scheme() as u8]);
911 bytes.extend_from_slice(signature.as_ref());
912 }
913 MultisigMemberSignature::Passkey(authenticator) => {
914 bytes.extend_from_slice(&authenticator.to_bytes());
915 }
916 }
917
918 Base64::encode_string(&bytes)
919 }
920
921 pub fn from_base64(s: &str) -> Result<Self, MultisigError> {
922 let bytes = Base64::decode_vec(s)?;
923
924 match bytes.first() {
925 Some(x) => {
926 if x == &(SignatureScheme::Ed25519 as u8) {
927 let signature = Ed25519Signature::from_bytes(&bytes[1..])?;
928 Ok(Self::Ed25519(signature))
929 } else if x == &(SignatureScheme::Secp256k1 as u8) {
930 let signature = Secp256k1Signature::from_bytes(&bytes[1..])?;
931 Ok(Self::Secp256k1(signature))
932 } else if x == &(SignatureScheme::Secp256r1 as u8) {
933 let signature = Secp256r1Signature::from_bytes(&bytes[1..])?;
934 Ok(Self::Secp256r1(signature))
935 } else if x == &(SignatureScheme::PasskeyAuthenticator as u8) {
936 let signature = PasskeyAuthenticator::from_bytes(&bytes[..])?;
937 Ok(Self::Passkey(signature))
938 } else {
939 Err(MultisigError::UnallowedSignatureType)
940 }
941 }
942 _ => Err(MultisigError::InvalidInput),
943 }
944 }
945 }
946
947 impl FromStr for MultisigMemberSignature {
948 type Err = MultisigError;
949
950 fn from_str(s: &str) -> Result<Self, Self::Err> {
951 Self::from_base64(s)
952 }
953 }
954}
955
956#[cfg(all(test, feature = "serde"))]
957mod tests {
958 use super::*;
959 use crate::UserSignature;
960
961 #[test]
964 fn passkey_multisig_roundtrip() {
965 let passkey_b64 = "BiVYDmenOnqS+thmz5m5SrZnWaKXZLVxgh+rri6LHXs25B0AAAAAnQF7InR5cGUiOiJ3ZWJhdXRobi5nZXQiLCAiY2hhbGxlbmdlIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MyIsImNyb3NzT3JpZ2luIjpmYWxzZSwgInVua25vd24iOiAidW5rbm93biJ9YgJMwqcOmZI7F/N+K5SMe4DRYCb4/cDWW68SFneSHoD2GxKKhksbpZ5rZpdrjSYABTCsFQQBpLORzTvbj4edWKd/AsEBeovrGvHR9Ku7critg6k7qvfFlPUngujXfEzXd8Eg";
966 let UserSignature::PasskeyAuthenticator(passkey_authenticator) =
967 UserSignature::from_base64(passkey_b64).unwrap()
968 else {
969 panic!("expected passkey authenticator");
970 };
971
972 let passkey_pk = passkey_authenticator.public_key();
973 let committee = MultisigCommittee::new(
974 vec![MultisigMember::new(PublicKey::Passkey(passkey_pk), 1)],
975 1,
976 )
977 .unwrap();
978 assert!(committee.validate().is_ok());
979
980 let aggregated = MultisigAggregatedSignature::new_unchecked(
981 vec![MultisigMemberSignature::Passkey(passkey_authenticator)],
982 0b1,
983 committee,
984 );
985
986 let bcs_bytes = bcs::to_bytes(&aggregated).unwrap();
987 let from_bcs: MultisigAggregatedSignature = bcs::from_bytes(&bcs_bytes).unwrap();
988 assert_eq!(aggregated, from_bcs);
989
990 let json = serde_json::to_string(&aggregated).unwrap();
991 let from_json: MultisigAggregatedSignature = serde_json::from_str(&json).unwrap();
992 assert_eq!(aggregated, from_json);
993 }
994
995 #[test]
996 fn test_to_indices() {
997 assert!(as_indices(0b11111111110).is_err());
998 assert_eq!(as_indices(0b0000010110).unwrap(), vec![1, 2, 4]);
999 assert_eq!(
1000 as_indices(0b1111111111).unwrap(),
1001 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1002 );
1003 }
1004
1005 #[test]
1009 fn new_rejects_out_of_order_signatures() {
1010 use crate::{Ed25519PublicKey, Ed25519Signature, SimpleSignature};
1011
1012 let pk0 = Ed25519PublicKey::new([1; 32]);
1013 let pk1 = Ed25519PublicKey::new([2; 32]);
1014 let pk2 = Ed25519PublicKey::new([3; 32]);
1015
1016 let committee = MultisigCommittee::new(
1017 vec![
1018 MultisigMember::new(pk0, 1),
1019 MultisigMember::new(pk1, 1),
1020 MultisigMember::new(pk2, 1),
1021 ],
1022 2,
1023 )
1024 .unwrap();
1025
1026 let dummy_sig = Ed25519Signature::new([0; 64]);
1027 let sig = |pk| {
1028 UserSignature::Simple(SimpleSignature::Ed25519 {
1029 signature: dummy_sig,
1030 public_key: pk,
1031 })
1032 };
1033
1034 let ok =
1036 MultisigAggregatedSignature::new(vec![sig(pk0), sig(pk2)], committee.clone()).unwrap();
1037 assert_eq!(ok.bitmap(), 0b101);
1038 assert_eq!(ok.signatures().len(), 2);
1039
1040 let err = MultisigAggregatedSignature::new(vec![sig(pk2), sig(pk0)], committee.clone())
1042 .unwrap_err();
1043 assert!(
1044 matches!(err, MultisigError::SignaturesOutOfOrder),
1045 "expected SignaturesOutOfOrder, got {err:?}"
1046 );
1047
1048 let err =
1050 MultisigAggregatedSignature::new(vec![sig(pk0), sig(pk0)], committee).unwrap_err();
1051 assert!(
1052 matches!(err, MultisigError::DuplicatePublicKey),
1053 "expected DuplicatePublicKey, got {err:?}"
1054 );
1055 }
1056
1057 #[test]
1058 fn member_signature_base64_roundtrip() {
1059 use crate::Ed25519Signature;
1060
1061 let sig = MultisigMemberSignature::Ed25519(Ed25519Signature::new([0xAB; 64]));
1062 let encoded = sig.to_base64();
1063 let decoded = MultisigMemberSignature::from_base64(&encoded)
1064 .expect("from_base64 should accept what to_base64 produced");
1065 assert_eq!(
1066 sig, decoded,
1067 "to_base64/from_base64 must be inverses of each other"
1068 );
1069 }
1070
1071 #[test]
1072 fn member_signature_base64_roundtrip_passkey() {
1073 let passkey_b64 = "BiVYDmenOnqS+thmz5m5SrZnWaKXZLVxgh+rri6LHXs25B0AAAAAnQF7InR5cGUiOiJ3ZWJhdXRobi5nZXQiLCAiY2hhbGxlbmdlIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MyIsImNyb3NzT3JpZ2luIjpmYWxzZSwgInVua25vd24iOiAidW5rbm93biJ9YgJMwqcOmZI7F/N+K5SMe4DRYCb4/cDWW68SFneSHoD2GxKKhksbpZ5rZpdrjSYABTCsFQQBpLORzTvbj4edWKd/AsEBeovrGvHR9Ku7critg6k7qvfFlPUngujXfEzXd8Eg";
1074 let UserSignature::PasskeyAuthenticator(passkey_authenticator) =
1075 UserSignature::from_base64(passkey_b64).unwrap()
1076 else {
1077 panic!("expected passkey authenticator");
1078 };
1079 let sig = MultisigMemberSignature::Passkey(passkey_authenticator);
1080 let encoded = sig.to_base64();
1081 let decoded = MultisigMemberSignature::from_base64(&encoded)
1082 .expect("from_base64 should accept what to_base64 produced");
1083 assert_eq!(
1084 sig, decoded,
1085 "to_base64/from_base64 must be inverses of each other"
1086 );
1087 }
1088
1089 #[test]
1094 fn multisig_standalone_bcs_framing() {
1095 use base64ct::{Base64, Encoding};
1096
1097 let multisig_b64 = "AwIA+zI2waYMirpLgCXsqGcuy+VPNToMkxYeBxkQVSgFdIS/TnAHQKs9FFAzHTfV2iSJuO25oIw5dnu9KEBSZwiqBQAf+R79IrKzolrY7mAM6TmE8T9sKk496ztesq0ao6a5BDFeH0QrIXJ68PZFAdEE86k3wh1WkeIYjxAMrIBpy9YBGAAFALurCXoe8wunaE2g6O2CtIZW4lSIf/fImJrdqRvZtlphAQCpMi2gCtRO9jNLl3bwc1x8IB/YYs1P6XpUEQbwzh2lGAEABYLKuo/1LUczMu80FayRheNYXavDC8l+QLX/s4S9i7QBAJYYQ5QWs/aT8+VwA+Vh3wswa90eqAaf6N4yzYUSXuTCAQBR6+eXlJ4GViO9z7QXRPSUaRPR9DlyDjc6S9lz61JNLgECAA==";
1101 let flag_and_body = Base64::decode_vec(multisig_b64).unwrap();
1102 let UserSignature::Multisig(sig) = UserSignature::from_bytes(&flag_and_body).unwrap()
1103 else {
1104 panic!("expected multisig signature");
1105 };
1106
1107 assert_eq!(
1111 sig.to_bytes(),
1112 flag_and_body,
1113 "multisig `to_bytes` must be the flat `flag || body` wire form"
1114 );
1115 assert_eq!(
1116 bcs::to_bytes(&sig).unwrap(),
1117 bcs::to_bytes(&flag_and_body).unwrap(),
1118 "standalone BCS must be the ULEB-length-prefixed `flag || body` wire form"
1119 );
1120 let decoded: MultisigAggregatedSignature =
1121 bcs::from_bytes(&bcs::to_bytes(&sig).unwrap()).unwrap();
1122 assert_eq!(sig, decoded);
1123 }
1124
1125 #[test]
1131 fn member_signature_passkey_bcs_framing() {
1132 use base64ct::{Base64, Encoding};
1133
1134 let passkey_b64 = "BiVYDmenOnqS+thmz5m5SrZnWaKXZLVxgh+rri6LHXs25B0AAAAAnQF7InR5cGUiOiJ3ZWJhdXRobi5nZXQiLCAiY2hhbGxlbmdlIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MyIsImNyb3NzT3JpZ2luIjpmYWxzZSwgInVua25vd24iOiAidW5rbm93biJ9YgJMwqcOmZI7F/N+K5SMe4DRYCb4/cDWW68SFneSHoD2GxKKhksbpZ5rZpdrjSYABTCsFQQBpLORzTvbj4edWKd/AsEBeovrGvHR9Ku7critg6k7qvfFlPUngujXfEzXd8Eg";
1135 let passkey_wire = Base64::decode_vec(passkey_b64).unwrap();
1136 let UserSignature::PasskeyAuthenticator(passkey_authenticator) =
1137 UserSignature::from_base64(passkey_b64).unwrap()
1138 else {
1139 panic!("expected passkey authenticator");
1140 };
1141
1142 let bytes =
1143 bcs::to_bytes(&MultisigMemberSignature::Passkey(passkey_authenticator)).unwrap();
1144 assert_eq!(bytes[0], 0x04, "passkey member signature must use tag 4");
1147 let expected = bcs::to_bytes(&passkey_wire).unwrap();
1148 assert_eq!(
1149 bytes[1..],
1150 expected[..],
1151 "passkey member payload must be the length-prefixed standalone wire form"
1152 );
1153 }
1154
1155 #[test]
1162 fn member_signature_bcs_tags() {
1163 use crate::Ed25519Signature;
1164
1165 let ed25519 = MultisigMemberSignature::Ed25519(Ed25519Signature::new([0xAB; 64]));
1166 assert_eq!(
1167 bcs::to_bytes(&ed25519).unwrap()[0],
1168 0x00,
1169 "ed25519 member must use BCS tag 0x00"
1170 );
1171
1172 let passkey_b64 = "BiVYDmenOnqS+thmz5m5SrZnWaKXZLVxgh+rri6LHXs25B0AAAAAnQF7InR5cGUiOiJ3ZWJhdXRobi5nZXQiLCAiY2hhbGxlbmdlIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MyIsImNyb3NzT3JpZ2luIjpmYWxzZSwgInVua25vd24iOiAidW5rbm93biJ9YgJMwqcOmZI7F/N+K5SMe4DRYCb4/cDWW68SFneSHoD2GxKKhksbpZ5rZpdrjSYABTCsFQQBpLORzTvbj4edWKd/AsEBeovrGvHR9Ku7critg6k7qvfFlPUngujXfEzXd8Eg";
1173 let UserSignature::PasskeyAuthenticator(passkey_authenticator) =
1174 UserSignature::from_base64(passkey_b64).unwrap()
1175 else {
1176 panic!("expected passkey authenticator");
1177 };
1178 let passkey = MultisigMemberSignature::Passkey(passkey_authenticator);
1179 assert_eq!(
1180 bcs::to_bytes(&passkey).unwrap()[0],
1181 0x04,
1182 "passkey member must use BCS tag 0x04"
1183 );
1184 }
1185
1186 #[test]
1187 fn validate_rejects_bitmap_bits_past_committee_size() {
1188 use crate::{Ed25519PublicKey, Ed25519Signature};
1189
1190 let pk0 = Ed25519PublicKey::new([1; 32]);
1191 let pk1 = Ed25519PublicKey::new([2; 32]);
1192 let committee = MultisigCommittee::new(
1193 vec![MultisigMember::new(pk0, 1), MultisigMember::new(pk1, 1)],
1194 2,
1195 )
1196 .unwrap();
1197
1198 let agg = MultisigAggregatedSignature::new_unchecked(
1199 vec![
1200 MultisigMemberSignature::Ed25519(Ed25519Signature::new([0; 64])),
1201 MultisigMemberSignature::Ed25519(Ed25519Signature::new([0; 64])),
1202 ],
1203 0b1100000000,
1204 committee,
1205 );
1206
1207 let result = agg.validate();
1208 assert!(
1209 result.is_err(),
1210 "validate() must reject bitmap bits outside committee.members.len(), got Ok"
1211 );
1212 }
1213
1214 #[test]
1215 fn new_rejects_duplicates() {
1216 use crate::{Ed25519PublicKey, Ed25519Signature, SimpleSignature};
1217
1218 let pk0 = Ed25519PublicKey::new([1; 32]);
1219 let pk1 = Ed25519PublicKey::new([2; 32]);
1220 let pk2 = Ed25519PublicKey::new([3; 32]);
1221
1222 let committee = MultisigCommittee::new(
1223 vec![
1224 MultisigMember::new(pk0, 1),
1225 MultisigMember::new(pk1, 1),
1226 MultisigMember::new(pk2, 1),
1227 ],
1228 2,
1229 )
1230 .unwrap();
1231
1232 let dummy = Ed25519Signature::new([0; 64]);
1233 let sig = |pk| {
1234 UserSignature::Simple(SimpleSignature::Ed25519 {
1235 signature: dummy,
1236 public_key: pk,
1237 })
1238 };
1239
1240 let err = MultisigAggregatedSignature::new(vec![sig(pk0), sig(pk0)], committee.clone())
1242 .unwrap_err();
1243 assert!(
1244 matches!(err, MultisigError::DuplicatePublicKey),
1245 "adjacent duplicate should be reported as DuplicatePublicKey, got {err:?}"
1246 );
1247
1248 let err = MultisigAggregatedSignature::new(vec![sig(pk0), sig(pk1), sig(pk0)], committee)
1251 .unwrap_err();
1252 assert!(
1253 matches!(err, MultisigError::DuplicatePublicKey),
1254 "non-adjacent duplicate should be reported as DuplicatePublicKey, got {err:?}"
1255 );
1256 }
1257
1258 #[test]
1261 fn committee_new_validation() {
1262 use crate::Ed25519PublicKey;
1263
1264 let member = |b: u8| MultisigMember::new(Ed25519PublicKey::new([b; 32]), 1);
1265
1266 assert!(MultisigCommittee::new(vec![member(1), member(2)], 2).is_ok());
1268
1269 assert!(matches!(
1271 MultisigCommittee::new(vec![member(1)], 0),
1272 Err(MultisigError::ZeroThreshold)
1273 ));
1274
1275 assert!(matches!(
1277 MultisigCommittee::new(vec![], 1),
1278 Err(MultisigError::EmptyCommittee)
1279 ));
1280
1281 assert!(matches!(
1283 MultisigCommittee::new(
1284 vec![MultisigMember::new(Ed25519PublicKey::new([1; 32]), 0)],
1285 1
1286 ),
1287 Err(MultisigError::ZeroWeightMember)
1288 ));
1289
1290 assert!(matches!(
1292 MultisigCommittee::new(vec![member(1), member(2)], 3),
1293 Err(MultisigError::InsufficientWeight(2, 3))
1294 ));
1295
1296 assert!(matches!(
1298 MultisigCommittee::new(vec![member(1), member(1)], 1),
1299 Err(MultisigError::DuplicatePublicKey)
1300 ));
1301
1302 let too_many = (0..=MULTISIG_COMMITTEE_SIZE_MAX as u8)
1304 .map(member)
1305 .collect();
1306 assert!(matches!(
1307 MultisigCommittee::new(too_many, 1),
1308 Err(MultisigError::CommitteeTooLarge(n)) if n == MULTISIG_COMMITTEE_SIZE_MAX + 1
1309 ));
1310 }
1311
1312 #[cfg(feature = "hash")]
1316 #[test]
1317 fn derive_address_is_stable() {
1318 use crate::{Address, Ed25519PublicKey, Secp256k1PublicKey, Secp256r1PublicKey};
1319
1320 let committee = MultisigCommittee::new(
1321 vec![
1322 MultisigMember::new(Ed25519PublicKey::new([1; 32]), 1),
1323 MultisigMember::new(Secp256k1PublicKey::new([2; 33]), 2),
1324 MultisigMember::new(Secp256r1PublicKey::new([3; 33]), 3),
1325 ],
1326 2,
1327 )
1328 .unwrap();
1329
1330 assert_eq!(
1331 committee.derive_address(),
1332 Address::from_hex("0x391d9897d470cda2a489f59b54a04f2d8fa7bcb9c1ac978872689b477909cffe")
1333 .unwrap()
1334 );
1335 }
1336
1337 #[test]
1339 fn new_rejects_invalid_signature_count() {
1340 use crate::{Ed25519PublicKey, Ed25519Signature, SimpleSignature};
1341
1342 let committee = MultisigCommittee::new(
1343 vec![
1344 MultisigMember::new(Ed25519PublicKey::new([1; 32]), 1),
1345 MultisigMember::new(Ed25519PublicKey::new([2; 32]), 1),
1346 ],
1347 2,
1348 )
1349 .unwrap();
1350
1351 let dummy = Ed25519Signature::new([0; 64]);
1352 let sig = |b: u8| {
1353 UserSignature::Simple(SimpleSignature::Ed25519 {
1354 signature: dummy,
1355 public_key: Ed25519PublicKey::new([b; 32]),
1356 })
1357 };
1358
1359 assert!(matches!(
1361 MultisigAggregatedSignature::new(vec![], committee.clone()),
1362 Err(MultisigError::InvalidSignatureNumber)
1363 ));
1364
1365 assert!(matches!(
1367 MultisigAggregatedSignature::new(vec![sig(1), sig(2), sig(3)], committee),
1368 Err(MultisigError::InvalidSignatureNumber)
1369 ));
1370 }
1371
1372 #[test]
1375 fn aggregated_signature_getters() {
1376 use crate::{Ed25519PublicKey, Ed25519Signature, SimpleSignature};
1377
1378 let pk0 = Ed25519PublicKey::new([1; 32]);
1379 let pk1 = Ed25519PublicKey::new([2; 32]);
1380 let committee = MultisigCommittee::new(
1381 vec![MultisigMember::new(pk0, 1), MultisigMember::new(pk1, 1)],
1382 2,
1383 )
1384 .unwrap();
1385
1386 let dummy = Ed25519Signature::new([7; 64]);
1387 let sig = |pk| {
1388 UserSignature::Simple(SimpleSignature::Ed25519 {
1389 signature: dummy,
1390 public_key: pk,
1391 })
1392 };
1393 let aggregated =
1394 MultisigAggregatedSignature::new(vec![sig(pk0), sig(pk1)], committee.clone()).unwrap();
1395
1396 assert_eq!(aggregated.committee(), &committee);
1397 assert_eq!(aggregated.bitmap(), 0b11);
1398 assert_eq!(aggregated.signatures().len(), 2);
1399 assert_eq!(aggregated.indices().unwrap(), vec![0, 1]);
1400 }
1401}