Skip to main content

sui_sdk_types/crypto/
multisig.rs

1use super::Ed25519PublicKey;
2use super::Ed25519Signature;
3use super::MlDsa65PublicKey;
4use super::MlDsa65Signature;
5use super::PasskeyAuthenticator;
6use super::PasskeyPublicKey;
7use super::Secp256k1PublicKey;
8use super::Secp256k1Signature;
9use super::Secp256r1PublicKey;
10use super::Secp256r1Signature;
11use super::SignatureScheme;
12use super::zklogin::ZkLoginAuthenticator;
13use super::zklogin::ZkLoginPublicIdentifier;
14
15pub type WeightUnit = u8;
16pub type ThresholdUnit = u16;
17pub type BitmapUnit = u16;
18
19const MAX_COMMITTEE_SIZE: usize = 10;
20// TODO validate sigs
21// const MAX_BITMAP_VALUE: BitmapUnit = 0b1111111111;
22
23/// Enum of valid public keys for multisig committee members
24///
25/// # BCS
26///
27/// The BCS serialized form for this type is defined by the following ABNF:
28///
29/// ```text
30/// multisig-member-public-key = ed25519-multisig-member-public-key /
31///                              secp256k1-multisig-member-public-key /
32///                              secp256r1-multisig-member-public-key /
33///                              zklogin-multisig-member-public-key
34///
35/// ed25519-multisig-member-public-key   = %x00 ed25519-public-key
36/// secp256k1-multisig-member-public-key = %x01 secp256k1-public-key
37/// secp256r1-multisig-member-public-key = %x02 secp256r1-public-key
38/// zklogin-multisig-member-public-key   = %x03 zklogin-public-identifier
39/// ```
40///
41/// There is also a legacy encoding for this type defined as:
42///
43/// ```text
44/// legacy-multisig-member-public-key = string ; which is valid base64 encoded
45///                                            ; and the decoded bytes are defined
46///                                            ; by legacy-public-key
47/// legacy-public-key = (ed25519-flag ed25519-public-key) /
48///                     (secp256k1-flag secp256k1-public-key) /
49///                     (secp256r1-flag secp256r1-public-key)
50/// ```
51#[derive(Clone, Debug, PartialEq, Eq)]
52#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
53#[non_exhaustive]
54pub enum MultisigMemberPublicKey {
55    Ed25519(Ed25519PublicKey),
56    Secp256k1(Secp256k1PublicKey),
57    Secp256r1(Secp256r1PublicKey),
58    ZkLogin(ZkLoginPublicIdentifier),
59    Passkey(PasskeyPublicKey),
60    MlDsa65(Box<MlDsa65PublicKey>),
61}
62
63/// A member in a multisig committee
64///
65/// # BCS
66///
67/// The BCS serialized form for this type is defined by the following ABNF:
68///
69/// ```text
70/// multisig-member = multisig-member-public-key
71///                   u8    ; weight
72/// ```
73///
74/// There is also a legacy encoding for this type defined as:
75///
76/// ```text
77/// legacy-multisig-member = legacy-multisig-member-public-key
78///                          u8     ; weight
79/// ```
80#[derive(Clone, Debug, PartialEq, Eq)]
81#[cfg_attr(
82    feature = "serde",
83    derive(serde_derive::Serialize, serde_derive::Deserialize)
84)]
85#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
86pub struct MultisigMember {
87    public_key: MultisigMemberPublicKey,
88    weight: WeightUnit,
89}
90
91impl MultisigMember {
92    /// Construct a new member from a `MultisigMemberPublicKey` and a `weight`.
93    pub fn new(public_key: MultisigMemberPublicKey, weight: WeightUnit) -> Self {
94        Self { public_key, weight }
95    }
96
97    /// This member's public key.
98    pub fn public_key(&self) -> &MultisigMemberPublicKey {
99        &self.public_key
100    }
101
102    /// Weight of this member's signature.
103    pub fn weight(&self) -> WeightUnit {
104        self.weight
105    }
106}
107
108/// A multisig committee
109///
110/// A `MultisigCommittee` is a set of members who collectively control a single `Address` on the
111/// Sui blockchain. The number of required signautres to authorize the execution of a transaction
112/// is determined by `(signature_0_weight + signature_1_weight ..) >= threshold`.
113///
114/// # Validity
115///
116/// Deserialization (BCS, JSON, or `from_serialized_bytes` on a containing
117/// `MultisigAggregatedSignature`) does **not** enforce structural validity:
118/// the resulting committee may have zero members, zero threshold, threshold
119/// greater than the sum of weights, duplicate members, or more than the
120/// `MAX_COMMITTEE_SIZE` limit. Validity is checked downstream by the
121/// verifier in `sui-crypto` before any signature is verified.
122///
123/// Consumers who inspect a deserialized committee — counting members,
124/// summing weights, indexing by bitmap, etc. — without first running
125/// signature verification **must** call [`MultisigCommittee::is_valid`]
126/// and reject the committee if it returns `false`. Skipping this check
127/// can cause downstream code to operate on attacker-supplied,
128/// well-formed-looking but malformed committees.
129///
130/// # BCS
131///
132/// The BCS serialized form for this type is defined by the following ABNF:
133///
134/// ```text
135/// multisig-committee = (vector multisig-member)
136///                      u16    ; threshold
137/// ```
138///
139/// There is also a legacy encoding for this type defined as:
140///
141/// ```text
142/// legacy-multisig-committee = (vector legacy-multisig-member)
143///                             u16     ; threshold
144/// ```
145#[derive(Debug, Clone, PartialEq, Eq)]
146#[cfg_attr(
147    feature = "serde",
148    derive(serde_derive::Serialize, serde_derive::Deserialize)
149)]
150#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
151pub struct MultisigCommittee {
152    /// A list of committee members and their corresponding weight.
153    #[cfg_attr(feature = "proptest", any(proptest::collection::size_range(0..=10).lift()))]
154    members: Vec<MultisigMember>,
155
156    /// If the total weight of the public keys corresponding to verified signatures is larger than
157    /// threshold, the Multisig is verified.
158    threshold: ThresholdUnit,
159}
160
161impl MultisigCommittee {
162    /// Construct a new committee from a list of `MultisigMember`s and a `threshold`.
163    ///
164    /// Note that the order of the members is significant towards deriving the `Address` governed
165    /// by this committee.
166    pub fn new(members: Vec<MultisigMember>, threshold: ThresholdUnit) -> Self {
167        Self { members, threshold }
168    }
169
170    /// The members of the committee
171    pub fn members(&self) -> &[MultisigMember] {
172        &self.members
173    }
174
175    /// The total signature weight required to authorize a transaction for the address
176    /// corresponding to this `MultisigCommittee`.
177    pub fn threshold(&self) -> ThresholdUnit {
178        self.threshold
179    }
180
181    /// Return the flag for this signature scheme
182    pub fn scheme(&self) -> SignatureScheme {
183        SignatureScheme::Multisig
184    }
185
186    /// Checks if the Committee is valid.
187    ///
188    /// A valid committee is one that:
189    ///  - Has a nonzero threshold
190    ///  - Has at least one member
191    ///  - Has at most ten members
192    ///  - No member has weight 0
193    ///  - the sum of the weights of all members must be larger than the threshold
194    ///  - contains no duplicate members
195    pub fn is_valid(&self) -> bool {
196        self.threshold != 0
197            && !self.members.is_empty()
198            && self.members.len() <= MAX_COMMITTEE_SIZE
199            && !self.members.iter().any(|member| member.weight == 0)
200            && self
201                .members
202                .iter()
203                .map(|member| member.weight as ThresholdUnit)
204                .sum::<ThresholdUnit>()
205                >= self.threshold
206            && !self.members.iter().enumerate().any(|(i, member)| {
207                self.members
208                    .iter()
209                    .skip(i + 1)
210                    .any(|m| member.public_key == m.public_key)
211            })
212    }
213}
214
215/// Aggregated signature from members of a multisig committee.
216///
217/// # BCS
218///
219/// The BCS serialized form for this type is defined by the following ABNF:
220///
221/// ```text
222/// multisig-aggregated-signature = (vector multisig-member-signature)
223///                                 u16     ; bitmap
224///                                 multisig-committee
225/// ```
226///
227/// There is also a legacy encoding for this type defined as:
228///
229/// ```text
230/// legacy-multisig-aggregated-signature = (vector multisig-member-signature)
231///                                        roaring-bitmap   ; bitmap
232///                                        legacy-multisig-committee
233/// roaring-bitmap = bytes  ; where the contents of the bytes are valid
234///                         ; according to the serialized spec for
235///                         ; roaring bitmaps
236/// ```
237///
238/// See [here](https://github.com/RoaringBitmap/RoaringFormatSpec) for the specification for the
239/// serialized format of RoaringBitmaps.
240#[derive(Debug, Clone)]
241#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
242pub struct MultisigAggregatedSignature {
243    /// The plain signature encoded with signature scheme.
244    ///
245    /// The signatures must be in the same order as they are listed in the committee.
246    #[cfg_attr(feature = "proptest", any(proptest::collection::size_range(0..=10).lift()))]
247    signatures: Vec<MultisigMemberSignature>,
248    /// A bitmap that indicates the position of which public key the signature should be
249    /// authenticated with.
250    bitmap: BitmapUnit,
251    /// Legacy encoding for the bitmap.
252    //TODO implement a strategy for legacy bitmap
253    #[cfg_attr(feature = "proptest", strategy(proptest::strategy::Just(None)))]
254    legacy_bitmap: Option<crate::Bitmap>,
255    /// The public key encoded with each public key with its signature scheme used along with the
256    /// corresponding weight.
257    committee: MultisigCommittee,
258}
259
260impl MultisigAggregatedSignature {
261    /// Construct a new aggregated multisig signature.
262    ///
263    /// Since the list of signatures doesn't contain sufficient information to identify which
264    /// committee member provided the signature, it is up to the caller to ensure that the provided
265    /// signature list is in the same order as it's corresponding member in the provided committee
266    /// and that it's position in the provided bitmap is set.
267    pub fn new(
268        committee: MultisigCommittee,
269        signatures: Vec<MultisigMemberSignature>,
270        bitmap: BitmapUnit,
271    ) -> Self {
272        Self {
273            signatures,
274            bitmap,
275            legacy_bitmap: None,
276            committee,
277        }
278    }
279
280    /// The list of signatures from committee members
281    pub fn signatures(&self) -> &[MultisigMemberSignature] {
282        &self.signatures
283    }
284
285    /// The bitmap that indicates which committee members provided their signature.
286    pub fn bitmap(&self) -> BitmapUnit {
287        self.bitmap
288    }
289
290    /// The legacy roaring bitmap, if this is a legacy formatted signature
291    pub fn legacy_bitmap(&self) -> Option<&crate::Bitmap> {
292        self.legacy_bitmap.as_ref()
293    }
294
295    /// Configure with a legacy roaring bitmap
296    #[deprecated(note = "use `try_with_legacy_bitmap`, which rejects unserializable bitmaps")]
297    pub fn with_legacy_bitmap(&mut self, legacy_bitmap: crate::Bitmap) {
298        self.legacy_bitmap = Some(legacy_bitmap);
299    }
300
301    /// Configure with a legacy roaring bitmap.
302    ///
303    /// When a legacy bitmap is present, this signature serializes in the
304    /// legacy format, which carries the signer set only as the roaring
305    /// bitmap and can only encode Ed25519, Secp256k1, and Secp256r1
306    /// committee members. Returns an error, leaving `self` unchanged, if
307    /// `legacy_bitmap` does not encode the same signer set as
308    /// [`Self::bitmap`] or if the committee has a member that the legacy
309    /// format cannot encode.
310    pub fn try_with_legacy_bitmap(
311        &mut self,
312        legacy_bitmap: crate::Bitmap,
313    ) -> Result<(), InvalidLegacyBitmapError> {
314        validate_legacy_bitmap(&self.committee, self.bitmap, &legacy_bitmap)?;
315        self.legacy_bitmap = Some(legacy_bitmap);
316        Ok(())
317    }
318
319    /// The committee for this aggregated signature
320    pub fn committee(&self) -> &MultisigCommittee {
321        &self.committee
322    }
323}
324
325impl PartialEq for MultisigAggregatedSignature {
326    fn eq(&self, other: &Self) -> bool {
327        // Compare every field, including `legacy_bitmap`. Although the
328        // legacy bitmap is logically redundant with `bitmap` (they encode
329        // the same information in different formats), `to_bytes` prefers
330        // the legacy form whenever it is `Some`, so two signatures that
331        // differ only by `legacy_bitmap` will serialize to different byte
332        // strings. Excluding `legacy_bitmap` from `==` would let downstream
333        // consumers observe values where `a == b` but
334        // `bcs::to_bytes(a) != bcs::to_bytes(b)`, breaking standard
335        // Eq/Serialize expectations.
336        self.bitmap == other.bitmap
337            && self.legacy_bitmap == other.legacy_bitmap
338            && self.signatures == other.signatures
339            && self.committee == other.committee
340    }
341}
342
343impl Eq for MultisigAggregatedSignature {}
344
345/// Convert a roaring bitmap to plain bitmap.
346fn roaring_bitmap_to_u16(roaring: &crate::Bitmap) -> Result<BitmapUnit, &'static str> {
347    let mut val = 0;
348    for i in roaring.iter() {
349        if i >= MAX_COMMITTEE_SIZE as u32 {
350            return Err("invalid bitmap");
351        }
352        val |= 1 << i as u8;
353    }
354    Ok(val)
355}
356
357/// Check that `legacy_bitmap` can be attached to a multisig with the given
358/// `committee` and `bitmap`.
359///
360/// The BCS legacy decoder always derives `bitmap` from the roaring bitmap, so
361/// requiring the same here keeps `bitmap()` and `to_bytes()` from reporting
362/// two different signer sets for one signature. The legacy member encoding
363/// (`Base64MultisigMemberPublicKey`) only supports Ed25519, Secp256k1, and
364/// Secp256r1, so any other member would make `to_bytes()` fail to serialize
365/// the legacy form and panic.
366fn validate_legacy_bitmap(
367    committee: &MultisigCommittee,
368    bitmap: BitmapUnit,
369    legacy_bitmap: &crate::Bitmap,
370) -> Result<(), InvalidLegacyBitmapError> {
371    let derived = roaring_bitmap_to_u16(legacy_bitmap).map_err(InvalidLegacyBitmapError)?;
372    if derived != bitmap {
373        return Err(InvalidLegacyBitmapError(
374            "bitmap does not match legacy_bitmap",
375        ));
376    }
377
378    for member in &committee.members {
379        let reason = match member.public_key {
380            MultisigMemberPublicKey::Ed25519(_)
381            | MultisigMemberPublicKey::Secp256k1(_)
382            | MultisigMemberPublicKey::Secp256r1(_) => continue,
383            MultisigMemberPublicKey::ZkLogin(_) => {
384                "zklogin member is not representable in legacy multisig"
385            }
386            MultisigMemberPublicKey::Passkey(_) => {
387                "passkey member is not representable in legacy multisig"
388            }
389            MultisigMemberPublicKey::MlDsa65(_) => {
390                "mldsa65 member is not representable in legacy multisig"
391            }
392        };
393        return Err(InvalidLegacyBitmapError(reason));
394    }
395
396    Ok(())
397}
398
399/// Error returned when a legacy roaring bitmap cannot be attached to a
400/// [`MultisigAggregatedSignature`].
401#[derive(Debug, Clone, PartialEq, Eq)]
402pub struct InvalidLegacyBitmapError(&'static str);
403
404impl std::fmt::Display for InvalidLegacyBitmapError {
405    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406        f.write_str(self.0)
407    }
408}
409
410impl std::error::Error for InvalidLegacyBitmapError {}
411
412/// A signature from a member of a multisig committee.
413///
414/// # BCS
415///
416/// The BCS serialized form for this type is defined by the following ABNF:
417///
418/// ```text
419/// multisig-member-signature = ed25519-multisig-member-signature /
420///                             secp256k1-multisig-member-signature /
421///                             secp256r1-multisig-member-signature /
422///                             zklogin-multisig-member-signature
423///
424/// ed25519-multisig-member-signature   = %x00 ed25519-signature
425/// secp256k1-multisig-member-signature = %x01 secp256k1-signature
426/// secp256r1-multisig-member-signature = %x02 secp256r1-signature
427/// zklogin-multisig-member-signature   = %x03 zklogin-authenticator
428/// ```
429#[derive(Debug, Clone, PartialEq, Eq)]
430#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
431#[non_exhaustive]
432pub enum MultisigMemberSignature {
433    Ed25519(Ed25519Signature),
434    Secp256k1(Secp256k1Signature),
435    Secp256r1(Secp256r1Signature),
436    ZkLogin(Box<ZkLoginAuthenticator>),
437    Passkey(PasskeyAuthenticator),
438    MlDsa65(Box<MlDsa65Signature>),
439}
440
441#[cfg(feature = "serde")]
442#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
443mod serialization {
444    use super::*;
445    use crate::Ed25519PublicKey;
446    use crate::PasskeyPublicKey;
447    use crate::Secp256k1PublicKey;
448    use crate::Secp256r1PublicKey;
449    use crate::SignatureScheme;
450    use crate::crypto::Base64Array33;
451    use crate::crypto::Base64Array34;
452    use base64ct::Base64;
453    use base64ct::Encoding;
454    use serde::Deserialize;
455    use serde::Deserializer;
456    use serde::Serialize;
457    use serde::Serializer;
458    use serde_with::Bytes;
459    use serde_with::DeserializeAs;
460    use serde_with::SerializeAs;
461    use std::borrow::Cow;
462
463    pub struct Base64MultisigMemberPublicKey;
464
465    impl SerializeAs<MultisigMemberPublicKey> for Base64MultisigMemberPublicKey {
466        fn serialize_as<S>(
467            source: &MultisigMemberPublicKey,
468            serializer: S,
469        ) -> Result<S::Ok, S::Error>
470        where
471            S: Serializer,
472        {
473            match source {
474                MultisigMemberPublicKey::Ed25519(public_key) => {
475                    let mut buf = [0; 1 + Ed25519PublicKey::LENGTH];
476                    buf[0] = SignatureScheme::Ed25519 as u8;
477                    buf[1..].copy_from_slice(public_key.as_ref());
478                    Base64Array33::serialize_as(&buf, serializer)
479                }
480                MultisigMemberPublicKey::Secp256k1(public_key) => {
481                    let mut buf = [0; 1 + Secp256k1PublicKey::LENGTH];
482                    buf[0] = SignatureScheme::Secp256k1 as u8;
483                    buf[1..].copy_from_slice(public_key.as_ref());
484                    Base64Array34::serialize_as(&buf, serializer)
485                }
486                MultisigMemberPublicKey::Secp256r1(public_key) => {
487                    let mut buf = [0; 1 + Secp256r1PublicKey::LENGTH];
488                    buf[0] = SignatureScheme::Secp256r1 as u8;
489                    buf[1..].copy_from_slice(public_key.as_ref());
490                    Base64Array34::serialize_as(&buf, serializer)
491                }
492                MultisigMemberPublicKey::ZkLogin(_) => Err(serde::ser::Error::custom(
493                    "zklogin not supported in legacy multisig",
494                )),
495                MultisigMemberPublicKey::Passkey(_) => Err(serde::ser::Error::custom(
496                    "passkey not supported in legacy multisig",
497                )),
498                MultisigMemberPublicKey::MlDsa65(_) => Err(serde::ser::Error::custom(
499                    "mldsa65 not supported in legacy multisig",
500                )),
501            }
502        }
503    }
504
505    impl<'de> DeserializeAs<'de, MultisigMemberPublicKey> for Base64MultisigMemberPublicKey {
506        fn deserialize_as<D>(deserializer: D) -> Result<MultisigMemberPublicKey, D::Error>
507        where
508            D: Deserializer<'de>,
509        {
510            let b64: Cow<'de, str> = Deserialize::deserialize(deserializer)?;
511            let bytes = Base64::decode_vec(&b64).map_err(serde::de::Error::custom)?;
512            let flag = SignatureScheme::from_byte(
513                *bytes
514                    .first()
515                    .ok_or_else(|| serde::de::Error::custom("missing signature scheme flag"))?,
516            )
517            .map_err(serde::de::Error::custom)?;
518            let public_key_bytes = &bytes[1..];
519            match flag {
520                SignatureScheme::Ed25519 => {
521                    let public_key = Ed25519PublicKey::from_bytes(public_key_bytes)
522                        .map_err(serde::de::Error::custom)?;
523                    Ok(MultisigMemberPublicKey::Ed25519(public_key))
524                }
525                SignatureScheme::Secp256k1 => {
526                    let public_key = Secp256k1PublicKey::from_bytes(public_key_bytes)
527                        .map_err(serde::de::Error::custom)?;
528                    Ok(MultisigMemberPublicKey::Secp256k1(public_key))
529                }
530                SignatureScheme::Secp256r1 => {
531                    let public_key = Secp256r1PublicKey::from_bytes(public_key_bytes)
532                        .map_err(serde::de::Error::custom)?;
533                    Ok(MultisigMemberPublicKey::Secp256r1(public_key))
534                }
535                SignatureScheme::Multisig
536                | SignatureScheme::Bls12381
537                | SignatureScheme::ZkLogin
538                | SignatureScheme::Passkey
539                | SignatureScheme::MlDsa65 => {
540                    Err(serde::de::Error::custom("invalid public key type"))
541                }
542            }
543        }
544    }
545
546    pub struct LegacyMultisigMember;
547
548    impl SerializeAs<MultisigMember> for LegacyMultisigMember {
549        fn serialize_as<S>(source: &MultisigMember, serializer: S) -> Result<S::Ok, S::Error>
550        where
551            S: Serializer,
552        {
553            #[derive(serde_derive::Serialize)]
554            struct LegacyMember<'a> {
555                #[serde(with = "::serde_with::As::<Base64MultisigMemberPublicKey>")]
556                public_key: &'a MultisigMemberPublicKey,
557                weight: WeightUnit,
558            }
559
560            let legacy = LegacyMember {
561                public_key: &source.public_key,
562                weight: source.weight,
563            };
564
565            legacy.serialize(serializer)
566        }
567    }
568
569    impl<'de> DeserializeAs<'de, MultisigMember> for LegacyMultisigMember {
570        fn deserialize_as<D>(deserializer: D) -> Result<MultisigMember, D::Error>
571        where
572            D: Deserializer<'de>,
573        {
574            #[derive(serde_derive::Deserialize)]
575            struct LegacyMember {
576                #[serde(with = "::serde_with::As::<Base64MultisigMemberPublicKey>")]
577                public_key: MultisigMemberPublicKey,
578                weight: WeightUnit,
579            }
580
581            let legacy = LegacyMember::deserialize(deserializer)?;
582
583            Ok(MultisigMember {
584                public_key: legacy.public_key,
585                weight: legacy.weight,
586            })
587        }
588    }
589
590    #[derive(serde_derive::Deserialize)]
591    pub struct Multisig {
592        signatures: Vec<MultisigMemberSignature>,
593        bitmap: BitmapUnit,
594        committee: MultisigCommittee,
595    }
596
597    #[derive(serde_derive::Serialize)]
598    pub struct MultisigRef<'a> {
599        signatures: &'a [MultisigMemberSignature],
600        bitmap: BitmapUnit,
601        committee: &'a MultisigCommittee,
602    }
603
604    #[derive(serde_derive::Deserialize)]
605    pub struct LegacyMultisig {
606        signatures: Vec<MultisigMemberSignature>,
607        bitmap: crate::Bitmap,
608        committee: LegacyMultisigCommittee,
609    }
610
611    #[derive(serde_derive::Serialize)]
612    pub struct LegacyMultisigRef<'a> {
613        signatures: &'a [MultisigMemberSignature],
614        bitmap: &'a crate::Bitmap,
615        committee: LegacyMultisigCommitteeRef<'a>,
616    }
617
618    #[derive(serde_derive::Deserialize)]
619    struct LegacyMultisigCommittee {
620        #[serde(with = "::serde_with::As::<Vec<LegacyMultisigMember>>")]
621        members: Vec<MultisigMember>,
622        threshold: ThresholdUnit,
623    }
624
625    #[derive(serde_derive::Serialize)]
626    struct LegacyMultisigCommitteeRef<'a> {
627        #[serde(with = "::serde_with::As::<&[LegacyMultisigMember]>")]
628        members: &'a [MultisigMember],
629        threshold: ThresholdUnit,
630    }
631
632    #[derive(serde_derive::Deserialize)]
633    struct ReadableMultisigAggregatedSignature {
634        signatures: Vec<MultisigMemberSignature>,
635        bitmap: BitmapUnit,
636        legacy_bitmap: Option<crate::Bitmap>,
637        committee: MultisigCommittee,
638    }
639
640    #[derive(serde_derive::Serialize)]
641    struct ReadableMultisigAggregatedSignatureRef<'a> {
642        signatures: &'a [MultisigMemberSignature],
643        bitmap: BitmapUnit,
644        #[serde(skip_serializing_if = "Option::is_none")]
645        legacy_bitmap: &'a Option<crate::Bitmap>,
646        committee: &'a MultisigCommittee,
647    }
648
649    impl Serialize for MultisigAggregatedSignature {
650        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
651        where
652            S: Serializer,
653        {
654            if serializer.is_human_readable() {
655                let readable = ReadableMultisigAggregatedSignatureRef {
656                    signatures: &self.signatures,
657                    bitmap: self.bitmap,
658                    legacy_bitmap: &self.legacy_bitmap,
659                    committee: &self.committee,
660                };
661                readable.serialize(serializer)
662            } else {
663                let bytes = self.to_bytes();
664                serializer.serialize_bytes(&bytes)
665            }
666        }
667    }
668
669    impl<'de> Deserialize<'de> for MultisigAggregatedSignature {
670        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
671        where
672            D: Deserializer<'de>,
673        {
674            if deserializer.is_human_readable() {
675                let readable = ReadableMultisigAggregatedSignature::deserialize(deserializer)?;
676                // An untrusted JSON payload must not be able to produce a
677                // value whose `bitmap()` and `to_bytes()` disagree on the
678                // signer set, or whose `to_bytes()` panics.
679                if let Some(legacy_bitmap) = &readable.legacy_bitmap {
680                    validate_legacy_bitmap(&readable.committee, readable.bitmap, legacy_bitmap)
681                        .map_err(serde::de::Error::custom)?;
682                }
683                Ok(Self {
684                    signatures: readable.signatures,
685                    bitmap: readable.bitmap,
686                    legacy_bitmap: readable.legacy_bitmap,
687                    committee: readable.committee,
688                })
689            } else {
690                let bytes: Cow<'de, [u8]> = Bytes::deserialize_as(deserializer)?;
691                Self::from_serialized_bytes(bytes)
692            }
693        }
694    }
695
696    impl MultisigAggregatedSignature {
697        pub(crate) fn to_bytes(&self) -> Vec<u8> {
698            let mut buf = Vec::new();
699            buf.push(SignatureScheme::Multisig as u8);
700
701            if let Some(bitmap) = &self.legacy_bitmap {
702                let legacy = LegacyMultisigRef {
703                    signatures: &self.signatures,
704                    bitmap,
705                    committee: LegacyMultisigCommitteeRef {
706                        members: &self.committee.members,
707                        threshold: self.committee.threshold,
708                    },
709                };
710
711                bcs::serialize_into(&mut buf, &legacy).expect("serialization cannot fail");
712            } else {
713                let multisig = MultisigRef {
714                    signatures: &self.signatures,
715                    bitmap: self.bitmap,
716                    committee: &self.committee,
717                };
718                bcs::serialize_into(&mut buf, &multisig).expect("serialization cannot fail");
719            }
720            buf
721        }
722
723        pub(crate) fn from_serialized_bytes<T: AsRef<[u8]>, E: serde::de::Error>(
724            bytes: T,
725        ) -> Result<Self, E> {
726            let bytes = bytes.as_ref();
727            let flag = SignatureScheme::from_byte(
728                *bytes
729                    .first()
730                    .ok_or_else(|| serde::de::Error::custom("missing signature scheme flag"))?,
731            )
732            .map_err(serde::de::Error::custom)?;
733            if flag != SignatureScheme::Multisig {
734                return Err(serde::de::Error::custom("invalid multisig flag"));
735            }
736            let bcs_bytes = &bytes[1..];
737
738            // Unfortunately we have no information in the serialized form of a Multisig to be
739            // able to determine if its a Legacy format or the new standard format so we just
740            // need to try each.
741            //
742            // We'll start with the newer format as that should be more prevalent.
743            if let Ok(multisig) = bcs::from_bytes::<Multisig>(bcs_bytes) {
744                Ok(Self {
745                    signatures: multisig.signatures,
746                    bitmap: multisig.bitmap,
747                    legacy_bitmap: None,
748                    committee: multisig.committee,
749                })
750            } else if let Ok(legacy) = bcs::from_bytes::<LegacyMultisig>(bcs_bytes) {
751                Ok(Self {
752                    signatures: legacy.signatures,
753                    bitmap: roaring_bitmap_to_u16(&legacy.bitmap)
754                        .map_err(serde::de::Error::custom)?,
755                    legacy_bitmap: Some(legacy.bitmap),
756                    committee: MultisigCommittee {
757                        members: legacy.committee.members,
758                        threshold: legacy.committee.threshold,
759                    },
760                })
761            } else {
762                Err(serde::de::Error::custom("invalid multisig"))
763            }
764        }
765    }
766
767    #[derive(serde_derive::Serialize, serde_derive::Deserialize)]
768    enum MemberPublicKey {
769        Ed25519(Ed25519PublicKey),
770        Secp256k1(Secp256k1PublicKey),
771        Secp256r1(Secp256r1PublicKey),
772        ZkLogin(ZkLoginPublicIdentifier),
773        Passkey(PasskeyPublicKey),
774        MlDsa65(Box<MlDsa65PublicKey>),
775    }
776
777    #[derive(serde_derive::Serialize, serde_derive::Deserialize)]
778    #[serde(tag = "scheme", rename_all = "lowercase")]
779    #[serde(rename = "MultisigMemberPublicKey")]
780    enum ReadableMemberPublicKey {
781        Ed25519 { public_key: Ed25519PublicKey },
782        Secp256k1 { public_key: Secp256k1PublicKey },
783        Secp256r1 { public_key: Secp256r1PublicKey },
784        ZkLogin(ZkLoginPublicIdentifier),
785        Passkey { public_key: PasskeyPublicKey },
786        MlDsa65 { public_key: Box<MlDsa65PublicKey> },
787    }
788
789    impl Serialize for MultisigMemberPublicKey {
790        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
791        where
792            S: Serializer,
793        {
794            if serializer.is_human_readable() {
795                let readable = match self {
796                    MultisigMemberPublicKey::Ed25519(public_key) => {
797                        ReadableMemberPublicKey::Ed25519 {
798                            public_key: *public_key,
799                        }
800                    }
801                    MultisigMemberPublicKey::Secp256k1(public_key) => {
802                        ReadableMemberPublicKey::Secp256k1 {
803                            public_key: *public_key,
804                        }
805                    }
806                    MultisigMemberPublicKey::Secp256r1(public_key) => {
807                        ReadableMemberPublicKey::Secp256r1 {
808                            public_key: *public_key,
809                        }
810                    }
811                    MultisigMemberPublicKey::ZkLogin(public_id) => {
812                        ReadableMemberPublicKey::ZkLogin(public_id.clone())
813                    }
814                    MultisigMemberPublicKey::Passkey(public_key) => {
815                        ReadableMemberPublicKey::Passkey {
816                            public_key: *public_key,
817                        }
818                    }
819                    MultisigMemberPublicKey::MlDsa65(public_key) => {
820                        ReadableMemberPublicKey::MlDsa65 {
821                            public_key: public_key.clone(),
822                        }
823                    }
824                };
825                readable.serialize(serializer)
826            } else {
827                let binary = match self {
828                    MultisigMemberPublicKey::Ed25519(public_key) => {
829                        MemberPublicKey::Ed25519(*public_key)
830                    }
831                    MultisigMemberPublicKey::Secp256k1(public_key) => {
832                        MemberPublicKey::Secp256k1(*public_key)
833                    }
834                    MultisigMemberPublicKey::Secp256r1(public_key) => {
835                        MemberPublicKey::Secp256r1(*public_key)
836                    }
837                    MultisigMemberPublicKey::ZkLogin(public_id) => {
838                        MemberPublicKey::ZkLogin(public_id.clone())
839                    }
840                    MultisigMemberPublicKey::Passkey(public_key) => {
841                        MemberPublicKey::Passkey(*public_key)
842                    }
843                    MultisigMemberPublicKey::MlDsa65(public_key) => {
844                        MemberPublicKey::MlDsa65(public_key.clone())
845                    }
846                };
847                binary.serialize(serializer)
848            }
849        }
850    }
851
852    impl<'de> Deserialize<'de> for MultisigMemberPublicKey {
853        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
854        where
855            D: Deserializer<'de>,
856        {
857            if deserializer.is_human_readable() {
858                let readable = ReadableMemberPublicKey::deserialize(deserializer)?;
859                Ok(match readable {
860                    ReadableMemberPublicKey::Ed25519 { public_key } => Self::Ed25519(public_key),
861                    ReadableMemberPublicKey::Secp256k1 { public_key } => {
862                        Self::Secp256k1(public_key)
863                    }
864                    ReadableMemberPublicKey::Secp256r1 { public_key } => {
865                        Self::Secp256r1(public_key)
866                    }
867                    ReadableMemberPublicKey::ZkLogin(public_id) => Self::ZkLogin(public_id),
868                    ReadableMemberPublicKey::Passkey { public_key } => Self::Passkey(public_key),
869                    ReadableMemberPublicKey::MlDsa65 { public_key } => Self::MlDsa65(public_key),
870                })
871            } else {
872                let binary = MemberPublicKey::deserialize(deserializer)?;
873                Ok(match binary {
874                    MemberPublicKey::Ed25519(public_key) => Self::Ed25519(public_key),
875                    MemberPublicKey::Secp256k1(public_key) => Self::Secp256k1(public_key),
876                    MemberPublicKey::Secp256r1(public_key) => Self::Secp256r1(public_key),
877                    MemberPublicKey::ZkLogin(public_id) => Self::ZkLogin(public_id),
878                    MemberPublicKey::Passkey(public_key) => Self::Passkey(public_key),
879                    MemberPublicKey::MlDsa65(public_key) => Self::MlDsa65(public_key),
880                })
881            }
882        }
883    }
884
885    #[derive(serde_derive::Serialize, serde_derive::Deserialize)]
886    enum MemberSignature {
887        Ed25519(Ed25519Signature),
888        Secp256k1(Secp256k1Signature),
889        Secp256r1(Secp256r1Signature),
890        ZkLogin(Box<ZkLoginAuthenticator>),
891        Passkey(PasskeyAuthenticator),
892        MlDsa65(Box<MlDsa65Signature>),
893    }
894
895    #[derive(serde_derive::Serialize, serde_derive::Deserialize)]
896    #[serde(tag = "scheme", rename_all = "lowercase")]
897    #[serde(rename = "MultisigMemberSignature")]
898    enum ReadableMemberSignature {
899        Ed25519 { signature: Ed25519Signature },
900        Secp256k1 { signature: Secp256k1Signature },
901        Secp256r1 { signature: Secp256r1Signature },
902        ZkLogin(Box<ZkLoginAuthenticator>),
903        Passkey(PasskeyAuthenticator),
904        MlDsa65 { signature: Box<MlDsa65Signature> },
905    }
906
907    impl Serialize for MultisigMemberSignature {
908        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
909        where
910            S: Serializer,
911        {
912            if serializer.is_human_readable() {
913                let readable = match self {
914                    MultisigMemberSignature::Ed25519(signature) => {
915                        ReadableMemberSignature::Ed25519 {
916                            signature: *signature,
917                        }
918                    }
919                    MultisigMemberSignature::Secp256k1(signature) => {
920                        ReadableMemberSignature::Secp256k1 {
921                            signature: *signature,
922                        }
923                    }
924                    MultisigMemberSignature::Secp256r1(signature) => {
925                        ReadableMemberSignature::Secp256r1 {
926                            signature: *signature,
927                        }
928                    }
929                    MultisigMemberSignature::ZkLogin(authenticator) => {
930                        ReadableMemberSignature::ZkLogin(authenticator.clone())
931                    }
932                    MultisigMemberSignature::Passkey(authenticator) => {
933                        ReadableMemberSignature::Passkey(authenticator.clone())
934                    }
935                    MultisigMemberSignature::MlDsa65(signature) => {
936                        ReadableMemberSignature::MlDsa65 {
937                            signature: signature.clone(),
938                        }
939                    }
940                };
941                readable.serialize(serializer)
942            } else {
943                let binary = match self {
944                    MultisigMemberSignature::Ed25519(signature) => {
945                        MemberSignature::Ed25519(*signature)
946                    }
947                    MultisigMemberSignature::Secp256k1(signature) => {
948                        MemberSignature::Secp256k1(*signature)
949                    }
950                    MultisigMemberSignature::Secp256r1(signature) => {
951                        MemberSignature::Secp256r1(*signature)
952                    }
953                    MultisigMemberSignature::ZkLogin(authenticator) => {
954                        MemberSignature::ZkLogin(authenticator.clone())
955                    }
956                    MultisigMemberSignature::Passkey(authenticator) => {
957                        MemberSignature::Passkey(authenticator.clone())
958                    }
959                    MultisigMemberSignature::MlDsa65(signature) => {
960                        MemberSignature::MlDsa65(signature.clone())
961                    }
962                };
963                binary.serialize(serializer)
964            }
965        }
966    }
967
968    impl<'de> Deserialize<'de> for MultisigMemberSignature {
969        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
970        where
971            D: Deserializer<'de>,
972        {
973            if deserializer.is_human_readable() {
974                let readable = ReadableMemberSignature::deserialize(deserializer)?;
975                Ok(match readable {
976                    ReadableMemberSignature::Ed25519 { signature } => Self::Ed25519(signature),
977                    ReadableMemberSignature::Secp256k1 { signature } => Self::Secp256k1(signature),
978                    ReadableMemberSignature::Secp256r1 { signature } => Self::Secp256r1(signature),
979                    ReadableMemberSignature::ZkLogin(authenticator) => Self::ZkLogin(authenticator),
980                    ReadableMemberSignature::Passkey(authenticator) => Self::Passkey(authenticator),
981                    ReadableMemberSignature::MlDsa65 { signature } => Self::MlDsa65(signature),
982                })
983            } else {
984                let binary = MemberSignature::deserialize(deserializer)?;
985                Ok(match binary {
986                    MemberSignature::Ed25519(signature) => Self::Ed25519(signature),
987                    MemberSignature::Secp256k1(signature) => Self::Secp256k1(signature),
988                    MemberSignature::Secp256r1(signature) => Self::Secp256r1(signature),
989                    MemberSignature::ZkLogin(authenticator) => Self::ZkLogin(authenticator),
990                    MemberSignature::Passkey(authenticator) => Self::Passkey(authenticator),
991                    MemberSignature::MlDsa65(signature) => Self::MlDsa65(signature),
992                })
993            }
994        }
995    }
996}
997
998#[cfg(test)]
999mod test {
1000    use super::*;
1001
1002    #[cfg(target_arch = "wasm32")]
1003    use wasm_bindgen_test::wasm_bindgen_test as test;
1004
1005    // Regression test: `legacy_bitmap` used to be excluded from
1006    // `PartialEq`, so two signatures whose `to_bytes` output differed
1007    // (because the legacy form is preferred when present) could compare
1008    // equal. Equality must now imply byte equality.
1009    #[test]
1010    fn partial_eq_includes_legacy_bitmap() {
1011        let committee = MultisigCommittee::new(Vec::new(), 0);
1012        let a = MultisigAggregatedSignature::new(committee.clone(), Vec::new(), 0);
1013        let mut b = MultisigAggregatedSignature::new(committee, Vec::new(), 0);
1014        assert_eq!(a, b);
1015
1016        b.try_with_legacy_bitmap(crate::Bitmap::new()).unwrap();
1017        assert_ne!(a, b);
1018    }
1019
1020    fn ed25519_member(byte: u8) -> MultisigMember {
1021        MultisigMember::new(
1022            MultisigMemberPublicKey::Ed25519(Ed25519PublicKey::new(
1023                [byte; Ed25519PublicKey::LENGTH],
1024            )),
1025            1,
1026        )
1027    }
1028
1029    #[test]
1030    fn try_with_legacy_bitmap_rejects_mismatched_bitmap() {
1031        let committee = MultisigCommittee::new(vec![ed25519_member(1), ed25519_member(2)], 1);
1032        let mut signature = MultisigAggregatedSignature::new(committee, Vec::new(), 0b10);
1033
1034        let err = signature
1035            .try_with_legacy_bitmap([0].into_iter().collect())
1036            .unwrap_err();
1037        assert_eq!(err.to_string(), "bitmap does not match legacy_bitmap");
1038
1039        let err = signature
1040            .try_with_legacy_bitmap([MAX_COMMITTEE_SIZE as u32].into_iter().collect())
1041            .unwrap_err();
1042        assert_eq!(err.to_string(), "invalid bitmap");
1043
1044        assert_eq!(signature.legacy_bitmap(), None);
1045
1046        let legacy_bitmap: crate::Bitmap = [1].into_iter().collect();
1047        signature
1048            .try_with_legacy_bitmap(legacy_bitmap.clone())
1049            .unwrap();
1050        assert_eq!(signature.legacy_bitmap(), Some(&legacy_bitmap));
1051    }
1052
1053    // Regression test: attaching a legacy bitmap to a committee with a
1054    // member that has no legacy encoding made `to_bytes()` panic.
1055    #[test]
1056    fn try_with_legacy_bitmap_rejects_unrepresentable_members() {
1057        let zklogin = MultisigMemberPublicKey::ZkLogin(
1058            ZkLoginPublicIdentifier::new(
1059                "https://accounts.google.com".to_owned(),
1060                "7".parse().unwrap(),
1061            )
1062            .unwrap(),
1063        );
1064        let passkey = MultisigMemberPublicKey::Passkey(PasskeyPublicKey::new(
1065            Secp256r1PublicKey::new([2; Secp256r1PublicKey::LENGTH]),
1066        ));
1067        let mldsa65 = MultisigMemberPublicKey::MlDsa65(Box::new(MlDsa65PublicKey::new(
1068            [0; MlDsa65PublicKey::LENGTH],
1069        )));
1070
1071        for (public_key, scheme) in [
1072            (zklogin, "zklogin"),
1073            (passkey, "passkey"),
1074            (mldsa65, "mldsa65"),
1075        ] {
1076            let committee = MultisigCommittee::new(
1077                vec![ed25519_member(1), MultisigMember::new(public_key, 1)],
1078                1,
1079            );
1080            let mut signature = MultisigAggregatedSignature::new(committee, Vec::new(), 0b1);
1081
1082            let err = signature
1083                .try_with_legacy_bitmap([0].into_iter().collect())
1084                .unwrap_err();
1085            assert_eq!(
1086                err.to_string(),
1087                format!("{scheme} member is not representable in legacy multisig")
1088            );
1089            assert_eq!(signature.legacy_bitmap(), None);
1090        }
1091    }
1092
1093    #[cfg(feature = "serde")]
1094    #[test]
1095    fn try_with_legacy_bitmap_round_trips_through_bcs() {
1096        let committee = MultisigCommittee::new(vec![ed25519_member(1), ed25519_member(2)], 1);
1097        let mut signature = MultisigAggregatedSignature::new(
1098            committee,
1099            vec![MultisigMemberSignature::Ed25519(Ed25519Signature::new(
1100                [3; Ed25519Signature::LENGTH],
1101            ))],
1102            0b10,
1103        );
1104        signature
1105            .try_with_legacy_bitmap([1].into_iter().collect())
1106            .unwrap();
1107
1108        let signature = crate::UserSignature::Multisig(signature);
1109        let bytes = signature.to_bytes();
1110        assert_eq!(crate::UserSignature::from_bytes(&bytes).unwrap(), signature);
1111    }
1112
1113    // Regression test: the JSON deserializer used to copy `bitmap` and
1114    // `legacy_bitmap` straight onto the value without checking that
1115    // they encoded the same signer set, letting a single payload carry
1116    // one signer set observed by `bitmap()` and a different one emitted
1117    // by `to_bytes()` (which prefers the legacy form when present). The
1118    // BCS legacy branch always derives `bitmap` from `legacy_bitmap`, so
1119    // the JSON path must reject inputs where those two fields disagree.
1120    #[cfg(feature = "serde")]
1121    #[test]
1122    fn json_dual_bitmap_must_be_consistent() {
1123        let mut roaring = crate::Bitmap::new();
1124        roaring.insert(5);
1125        let legacy_b64 = {
1126            use base64ct::Encoding;
1127            let mut buf = Vec::new();
1128            roaring.serialize_into(&mut buf).unwrap();
1129            base64ct::Base64::encode_string(&buf)
1130        };
1131
1132        // `bitmap` claims signer 0, `legacy_bitmap` claims signer 5.
1133        let inconsistent = format!(
1134            r#"{{"signatures":[],"bitmap":1,"legacy_bitmap":"{legacy_b64}",
1135                "committee":{{"members":[],"threshold":0}}}}"#
1136        );
1137        let err = serde_json::from_str::<MultisigAggregatedSignature>(&inconsistent)
1138            .expect_err("inconsistent dual bitmap must be rejected");
1139        assert!(
1140            err.to_string().contains("legacy_bitmap"),
1141            "unexpected error: {err}"
1142        );
1143
1144        // The canonical form (bitmap derived from legacy_bitmap) is
1145        // accepted.
1146        let consistent = format!(
1147            r#"{{"signatures":[],"bitmap":{},"legacy_bitmap":"{legacy_b64}",
1148                "committee":{{"members":[],"threshold":0}}}}"#,
1149            1u16 << 5,
1150        );
1151        serde_json::from_str::<MultisigAggregatedSignature>(&consistent)
1152            .expect("consistent dual bitmap must be accepted");
1153    }
1154
1155    // Regression test: `to_bytes()` used to panic via
1156    // `.expect("serialization cannot fail")` when `legacy_bitmap` was
1157    // present alongside a ZkLogin or Passkey committee member, because
1158    // the legacy member encoding (`Base64MultisigMemberPublicKey`)
1159    // explicitly returns `Err` for those variants. The JSON
1160    // deserializer must reject the combination so an attacker cannot
1161    // craft a payload that crashes a consumer on its first `to_bytes()`.
1162    #[cfg(feature = "serde")]
1163    #[test]
1164    fn json_legacy_bitmap_with_zklogin_member_is_rejected() {
1165        let legacy_b64 = {
1166            use base64ct::Encoding;
1167            let mut buf = Vec::new();
1168            crate::Bitmap::new().serialize_into(&mut buf).unwrap();
1169            base64ct::Base64::encode_string(&buf)
1170        };
1171
1172        let payload = format!(
1173            r#"{{
1174                "signatures":[],
1175                "bitmap":0,
1176                "legacy_bitmap":"{legacy_b64}",
1177                "committee":{{
1178                    "members":[{{
1179                        "public_key":{{
1180                            "scheme":"zklogin",
1181                            "iss":"https://accounts.google.com",
1182                            "address_seed":"7"
1183                        }},
1184                        "weight":1
1185                    }}],
1186                    "threshold":1
1187                }}
1188            }}"#
1189        );
1190        let err = serde_json::from_str::<MultisigAggregatedSignature>(&payload)
1191            .expect_err("zklogin member with legacy bitmap must be rejected");
1192        assert!(
1193            err.to_string().contains("zklogin"),
1194            "unexpected error: {err}"
1195        );
1196    }
1197}