Skip to main content

iota_sdk_types/crypto/
multisig.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5#[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/// A member in a multisig committee
59///
60/// # BCS
61///
62/// The BCS serialized form for this type is defined by the following ABNF:
63///
64/// ```text
65/// multisig-member = multisig-member-public-key
66///                   u8    ; weight
67/// ```
68///
69/// There is also a legacy encoding for this type defined as:
70///
71/// ```text
72/// legacy-multisig-member = legacy-multisig-member-public-key
73///                          u8     ; weight
74/// ```
75#[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    /// Construct a new member from a [`PublicKey`] and a [`WeightUnit`].
87    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    /// This member's public key.
95    pub fn public_key(&self) -> &PublicKey {
96        &self.public_key
97    }
98
99    /// Weight of this member's signature.
100    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/// A multisig committee
114///
115/// A `MultisigCommittee` is a set of members who collectively control a single
116/// `Address` on the IOTA blockchain. The number of required signatures to
117/// authorize the execution of a transaction is determined by
118/// `(signature_0_weight + signature_1_weight ..) >= threshold`.
119///
120/// # BCS
121///
122/// The BCS serialized form for this type is defined by the following ABNF:
123///
124/// ```text
125/// multisig-committee = (vector multisig-member)
126///                      u16    ; threshold
127/// ```
128///
129/// There is also a legacy encoding for this type defined as:
130///
131/// ```text
132/// legacy-multisig-committee = (vector legacy-multisig-member)
133///                             u16     ; threshold
134/// ```
135#[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    /// A list of committee members and their corresponding weight.
140    members: Vec<MultisigMember>,
141    /// If the total weight of the public keys corresponding to verified
142    /// signatures is larger than threshold, the Multisig is verified.
143    #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "u16"))]
144    threshold: ThresholdUnit,
145}
146
147impl MultisigCommittee {
148    /// Construct a [`MultisigCommittee`] without validating the result.
149    ///
150    /// Unlike [`Self::new`], this performs no checks: the committee may
151    /// violate any of the invariants enforced by [`Self::validate`] (zero
152    /// threshold, empty or oversized member list, zero-weight members,
153    /// duplicate public keys, or a threshold exceeding the sum of weights).
154    ///
155    /// Note that the order of the members is significant towards deriving the
156    /// [`Address`] governed by this committee.
157    ///
158    /// Prefer [`Self::new`]; this constructor is intended for deserialization
159    /// paths and tests where the inputs are already known to be well-formed.
160    ///
161    /// [`Address`]: crate::Address
162    pub fn new_unchecked(members: Vec<MultisigMember>, threshold: ThresholdUnit) -> Self {
163        Self { members, threshold }
164    }
165
166    /// Construct a [`MultisigCommittee`] and verify it via [`Self::validate`].
167    ///
168    /// Compared to [`Self::new_unchecked`], this rejects committees that:
169    ///  - have a zero `threshold`;
170    ///  - contain zero or more than ten members;
171    ///  - contain a member with weight 0;
172    ///  - have a `threshold` greater than the sum of all member weights;
173    ///  - contain duplicate public keys.
174    ///
175    /// Note that the order of the members is significant towards deriving the
176    /// [`Address`] governed by this committee.
177    ///
178    /// [`Address`]: crate::Address
179    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    /// The members of the committee
191    pub fn members(&self) -> &[MultisigMember] {
192        &self.members
193    }
194
195    /// The total signature weight required to authorize a transaction for the
196    /// address corresponding to this `MultisigCommittee`.
197    pub fn threshold(&self) -> ThresholdUnit {
198        self.threshold
199    }
200
201    /// Return the flag for this signature scheme
202    pub fn scheme(&self) -> SignatureScheme {
203        SignatureScheme::Multisig
204    }
205
206    /// Get the index of a public key in the committee, if it is a member.
207    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    /// Checks if the Committee is valid.
215    ///
216    /// A valid committee is one that:
217    ///  - Has a nonzero threshold
218    ///  - Has at least one member
219    ///  - Has at most ten members
220    ///  - No member has weight 0
221    ///  - the sum of the weights of all members must be at least the threshold
222    ///  - contains no duplicate members
223    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/// Aggregated signature from members of a multisig committee.
271///
272/// # BCS
273///
274/// The BCS serialized form for this type is defined by the following ABNF:
275///
276/// ```text
277/// multisig-aggregated-signature = (vector multisig-member-signature)
278///                                 u16     ; bitmap
279///                                 multisig-committee
280/// ```
281///
282/// There is also a legacy encoding for this type defined as:
283///
284/// ```text
285/// legacy-multisig-aggregated-signature = (vector multisig-member-signature)
286///                                        roaring-bitmap   ; bitmap
287///                                        legacy-multisig-committee
288/// roaring-bitmap = bytes  ; where the contents of the bytes are valid
289///                         ; according to the serialized spec for
290///                         ; roaring bitmaps
291/// ```
292///
293/// See [here](https://github.com/RoaringBitmap/RoaringFormatSpec) for the specification for the
294/// serialized format of RoaringBitmaps.
295#[derive(Clone, Debug, Eq, Hash, PartialEq)]
296#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
297pub struct MultisigAggregatedSignature {
298    /// The plain signature encoded with signature scheme.
299    ///
300    /// The signatures must be in the same order as they are listed in the
301    /// committee.
302    signatures: Vec<MultisigMemberSignature>,
303    /// A bitmap that indicates the position of which public key the signature
304    /// should be authenticated with.
305    #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "u16"))]
306    bitmap: BitmapUnit,
307    /// The public key encoded with each public key with its signature scheme
308    /// used along with the corresponding weight.
309    committee: MultisigCommittee,
310}
311
312impl MultisigAggregatedSignature {
313    /// Construct a [`MultisigAggregatedSignature`] from its raw parts without
314    /// validation.
315    ///
316    /// Unlike [`Self::new`], this performs no checks: the `committee` is not
317    /// validated, the `bitmap` is trusted as-is, and the signatures are not
318    /// cross-referenced against the committee. The resulting value may be
319    /// rejected by [`Self::validate`] or by on-chain verification.
320    ///
321    /// The caller must ensure that:
322    ///  - `signatures` appear in the same order as their corresponding members
323    ///    in `committee` (e.g. for committee `[pk1, pk2, pk3, pk4, pk5]`, valid
324    ///    signature orderings include `[sig1, sig2, sig5]` but not `[sig2,
325    ///    sig1, sig5]`);
326    ///  - each contributing member's position is set in `bitmap`;
327    ///  - `committee` itself satisfies [`MultisigCommittee::validate`].
328    ///
329    /// Prefer [`Self::new`] when starting from [`UserSignature`]s; this
330    /// constructor is intended for deserialization paths and tests where the
331    /// inputs are already known to be well-formed.
332    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    /// Construct a [`MultisigAggregatedSignature`] from a list of
345    /// [`UserSignature`]s and a [`MultisigCommittee`].
346    ///
347    /// Compared to [`Self::new_unchecked`], this:
348    ///  - validates `committee` via [`MultisigCommittee::validate`];
349    ///  - converts each [`UserSignature`] into a [`MultisigMemberSignature`];
350    ///  - derives the `bitmap` by locating each signature's public key in the
351    ///    committee, rejecting duplicates and signatures from non-members;
352    ///  - rejects empty signature lists and lists longer than the committee;
353    ///  - rejects `signatures` that are not in committee order.
354    ///
355    /// `signatures` must appear in the same order as their corresponding
356    /// members in `committee`: for committee `[pk1, pk2, pk3, pk4, pk5]`,
357    /// `[sig1, sig2, sig5]` is accepted but `[sig2, sig1, sig5]` is rejected
358    /// with [`MultisigError::SignaturesOutOfOrder`].
359    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    /// Validates the structural integrity of this aggregated signature.
402    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    /// The list of signatures from committee members
421    pub fn signatures(&self) -> &[MultisigMemberSignature] {
422        &self.signatures
423    }
424
425    /// The bitmap that indicates which committee members provided their
426    /// signature.
427    pub fn bitmap(&self) -> BitmapUnit {
428        self.bitmap
429    }
430
431    /// The indices of the committee members that provided their signature,
432    /// derived from the [`bitmap`](Self::bitmap).
433    ///
434    /// For example, a bitmap of `0b10110` yields `[1, 2, 4]`. Returns
435    /// [`MultisigError::InvalidBitmap`] if the bitmap has bits set beyond the
436    /// maximum committee size.
437    pub fn indices(&self) -> Result<Vec<u8>, MultisigError> {
438        as_indices(self.bitmap)
439    }
440
441    /// The committee that authorizes this aggregated signature.
442    pub fn committee(&self) -> &MultisigCommittee {
443        &self.committee
444    }
445
446    /// Returns `true` if any of the member signatures uses the given signature
447    /// scheme.
448    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
469/// Interpret a bitmap of 01s as a list of indices that is set to 1s.
470/// e.g. 22 = 0b10110, then the result is [1, 2, 4].
471fn 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/// A signature from a member of a multisig committee.
485///
486/// # BCS
487///
488/// The BCS serialized form for this type is defined by the following ABNF:
489///
490/// ```text
491/// multisig-member-signature = ed25519-multisig-member-signature /
492///                             secp256k1-multisig-member-signature /
493///                             secp256r1-multisig-member-signature /
494///                             passkey-multisig-member-signature
495///
496/// ed25519-multisig-member-signature               = %d00 ed25519-signature
497/// secp256k1-multisig-member-signature             = %d01 secp256k1-signature
498/// secp256r1-multisig-member-signature             = %d02 secp256r1-signature
499/// passkey-multisig-member-signature               = %d04 passkey-authenticator
500/// ```
501#[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    /// Owned wire shape of an aggregated multisig: the flat
649    /// `signatures || bitmap || committee` body without the scheme flag or
650    /// length prefix. `UserSignatureBody::Multisig` embeds this type directly
651    /// (the enum tag supplies the flag and the outer `UserSignature` the
652    /// length prefix), while `MultisigAggregatedSignature`'s own serde keeps
653    /// the historical `bytes`-wrapped `flag || body` form.
654    #[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        /// Encode this aggregated signature as `flag || bcs-body`, where
740        /// `flag` is the multisig scheme byte (`0x03`).
741        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    /// Wire shape for `MultisigMemberSignature`.
770    ///
771    /// The `ZkLoginDeprecated` placeholder pins `Passkey` at flag `%d04`; it
772    /// is rejected on deserialization. The passkey arm serializes through
773    /// `PasskeyAuthenticator`'s own serde, i.e. as length-prefixed `bytes`
774    /// containing `passkey-flag || passkey-authenticator`.
775    #[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    /// Roundtrip a multisig committee and aggregated signature that include a
962    /// passkey member.
963    #[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    /// `MultisigAggregatedSignature::new` must reject `UserSignature`s that
1006    /// are not provided in committee order, since the resulting bitmap and
1007    /// signatures vector would otherwise misalign at verification time.
1008    #[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        // In-order input is accepted and the bitmap matches the indices used.
1035        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        // Out-of-order input is rejected.
1041        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        // Adjacent duplicates are reported as duplicates, not as ordering errors.
1049        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    /// A standalone `MultisigAggregatedSignature` BCS-serializes as
1090    /// length-prefixed bytes containing `multisig-flag || body`, exactly the
1091    /// `to_bytes` form. A round-trip test cannot detect losing (or doubling)
1092    /// that framing, so it is pinned here byte-by-byte.
1093    #[test]
1094    fn multisig_standalone_bcs_framing() {
1095        use base64ct::{Base64, Encoding};
1096
1097        // A multisig `UserSignature` from a mainnet transaction, in the flat
1098        // `[0x03 flag][body]` wire form (the `to_bytes` / explorer encoding):
1099        // https://explorer.iota.org/txblock/BUPSmkG8QZgr1NtVNjFaJMYsArkaWMECs7RHbEZRZUEU?network=https%3A%2F%2Findexer.mainnet.iota.cafe
1100        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        // `to_bytes` is the flat `flag || body`; the standalone BCS form wraps
1108        // that same blob in a ULEB length prefix (identical to BCS-encoding the
1109        // bytes themselves).
1110        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    /// A passkey member signature is BCS-framed as `%d04` followed by
1126    /// length-prefixed bytes containing `passkey-flag || passkey body` — the
1127    /// historical wrapping produced by `PasskeyAuthenticator`'s serde. A
1128    /// round-trip test cannot detect losing (or doubling) that framing, so it
1129    /// is pinned here byte-by-byte.
1130    #[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        // `%d04` member tag, then the passkey as length-prefixed bytes whose
1145        // contents are exactly the standalone `flag || body` wire form.
1146        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    /// The BCS tag of a multisig `MemberSignature` is the on-chain wire
1156    /// format, so the variant indices must stay fixed. A round-trip test
1157    /// can't catch a shift (encode and decode move together), so the tag
1158    /// values are pinned here against hardcoded expectations. In particular
1159    /// this guards the `ZkLoginDeprecated` placeholder kept at index `0x03`:
1160    /// removing it would silently shift `Passkey` from `0x04` to `0x03`.
1161    #[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        // pk0, pk0 — the same key appears twice in adjacent positions.
1241        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        // pk0, pk1, pk0 — pk0 reappears non-adjacently, which must also be
1249        // reported as a duplicate.
1250        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    /// Pin the committee validation rules enforced by
1259    /// [`MultisigCommittee::new`].
1260    #[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        // A well-formed committee is accepted.
1267        assert!(MultisigCommittee::new(vec![member(1), member(2)], 2).is_ok());
1268
1269        // Zero threshold.
1270        assert!(matches!(
1271            MultisigCommittee::new(vec![member(1)], 0),
1272            Err(MultisigError::ZeroThreshold)
1273        ));
1274
1275        // Empty committee.
1276        assert!(matches!(
1277            MultisigCommittee::new(vec![], 1),
1278            Err(MultisigError::EmptyCommittee)
1279        ));
1280
1281        // Zero-weight member.
1282        assert!(matches!(
1283            MultisigCommittee::new(
1284                vec![MultisigMember::new(Ed25519PublicKey::new([1; 32]), 0)],
1285                1
1286            ),
1287            Err(MultisigError::ZeroWeightMember)
1288        ));
1289
1290        // Threshold larger than the total weight.
1291        assert!(matches!(
1292            MultisigCommittee::new(vec![member(1), member(2)], 3),
1293            Err(MultisigError::InsufficientWeight(2, 3))
1294        ));
1295
1296        // Duplicate public keys.
1297        assert!(matches!(
1298            MultisigCommittee::new(vec![member(1), member(1)], 1),
1299            Err(MultisigError::DuplicatePublicKey)
1300        ));
1301
1302        // More than `MULTISIG_COMMITTEE_SIZE_MAX` members.
1303        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    /// Pin the multisig address derivation against accidental changes. A
1313    /// committee with a fixed set of public keys and weights must always map
1314    /// to the same address.
1315    #[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    /// `new` rejects empty signature lists and lists longer than the committee.
1338    #[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        // Empty signature list.
1360        assert!(matches!(
1361            MultisigAggregatedSignature::new(vec![], committee.clone()),
1362            Err(MultisigError::InvalidSignatureNumber)
1363        ));
1364
1365        // More signatures than committee members.
1366        assert!(matches!(
1367            MultisigAggregatedSignature::new(vec![sig(1), sig(2), sig(3)], committee),
1368            Err(MultisigError::InvalidSignatureNumber)
1369        ));
1370    }
1371
1372    /// The getters expose the committee, member signatures, bitmap, and the
1373    /// indices derived from the bitmap.
1374    #[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}