Skip to main content

miden_standards/account/auth/
approver.rs

1use alloc::collections::BTreeSet;
2use alloc::format;
3use alloc::vec::Vec;
4use core::num::NonZeroU32;
5
6use miden_protocol::account::auth::{AuthScheme, PublicKey, PublicKeyCommitment};
7use miden_protocol::errors::AccountError;
8
9// APPROVER
10// ================================================================================================
11
12/// A signer that can approve transactions, identified by its public key commitment and the
13/// signature scheme used to verify its signatures.
14///
15/// Note: an approver using [`AuthScheme::EcdsaK256Keccak`] discloses its public key and signature
16/// at proving time and therefore does not provide public-key privacy, regardless of the component
17/// it is used in (single-sig, multisig, or guarded multisig). See
18/// [`AuthScheme::EcdsaK256Keccak`] for details, and prefer [`AuthScheme::Falcon512Poseidon2`] if
19/// signer-key privacy is required.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct Approver {
22    pub_key: PublicKeyCommitment,
23    auth_scheme: AuthScheme,
24}
25
26impl Approver {
27    /// Creates a new [`Approver`] from the given public key commitment and signature scheme.
28    ///
29    /// # Security
30    ///
31    /// The `pub_key` commitment must have been derived under `auth_scheme`. This is not checked
32    /// here: a commitment is a bare digest that carries no record of the scheme that produced it,
33    /// and this constructor accepts a raw commitment (e.g. one rebuilt from stored account state)
34    /// without the originating key, so it cannot re-derive or verify the scheme. Pairing a
35    /// commitment with the wrong scheme is a self-inflicted misconfiguration: authentication
36    /// dispatches on the stored scheme alone and hashes the provided key under that scheme's hash
37    /// function, so a mismatched commitment can never be reproduced and the account becomes
38    /// permanently unauthenticatable.
39    ///
40    /// To keep the two consistent by construction, derive the approver from a [`PublicKey`] via the
41    /// [`From<&PublicKey>`](Approver::from) conversion, or use the typed constructors on
42    /// [`AuthSingleSig`](crate::account::auth::AuthSingleSig).
43    pub fn new(pub_key: PublicKeyCommitment, auth_scheme: AuthScheme) -> Self {
44        Self { pub_key, auth_scheme }
45    }
46
47    /// Returns the public key commitment of this approver.
48    pub fn pub_key(&self) -> PublicKeyCommitment {
49        self.pub_key
50    }
51
52    /// Returns the signature scheme of this approver.
53    pub fn auth_scheme(&self) -> AuthScheme {
54        self.auth_scheme
55    }
56}
57
58impl From<&PublicKey> for Approver {
59    fn from(pub_key: &PublicKey) -> Self {
60        Self::new(pub_key.to_commitment(), pub_key.auth_scheme())
61    }
62}
63
64// APPROVER SET
65// ================================================================================================
66
67/// A set of [`Approver`]s together with the threshold of signatures required to approve a
68/// transaction by default.
69///
70/// The set is guaranteed to be valid by construction: the threshold is non-zero, the number of
71/// approvers is at most [`ApproverSet::MAX_APPROVERS`], and no public key commitment appears more
72/// than once.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct ApproverSet {
75    approvers: Vec<Approver>,
76    threshold: NonZeroU32,
77}
78
79impl ApproverSet {
80    /// The maximum number of approvers a set may contain.
81    pub const MAX_APPROVERS: u8 = 64;
82
83    /// Creates a new [`ApproverSet`] from the given approvers and default threshold.
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if:
88    /// - `threshold` is zero,
89    /// - the number of approvers exceeds [`Self::MAX_APPROVERS`],
90    /// - `threshold` is greater than the number of approvers, or
91    /// - two approvers share the same public key commitment.
92    pub fn new(approvers: Vec<Approver>, threshold: u32) -> Result<Self, AccountError> {
93        let threshold = NonZeroU32::new(threshold)
94            .ok_or_else(|| AccountError::other("threshold must be at least 1"))?;
95
96        if approvers.len() as u64 > u64::from(Self::MAX_APPROVERS) {
97            return Err(AccountError::other(format!(
98                "number of approvers cannot be greater than {}",
99                Self::MAX_APPROVERS
100            )));
101        }
102
103        if threshold.get() > approvers.len() as u32 {
104            return Err(AccountError::other(
105                "threshold cannot be greater than number of approvers",
106            ));
107        }
108
109        let unique_approvers: BTreeSet<_> = approvers.iter().map(Approver::pub_key).collect();
110        if unique_approvers.len() != approvers.len() {
111            return Err(AccountError::other("duplicate approver public keys are not allowed"));
112        }
113
114        Ok(Self { approvers, threshold })
115    }
116
117    /// Returns the approvers in this set.
118    pub fn approvers(&self) -> &[Approver] {
119        &self.approvers
120    }
121
122    /// Returns the default threshold of signatures required to approve a transaction.
123    pub fn threshold(&self) -> NonZeroU32 {
124        self.threshold
125    }
126}
127
128// TESTS
129// ================================================================================================
130
131#[cfg(test)]
132mod tests {
133    use alloc::string::ToString;
134
135    use miden_protocol::Word;
136    use miden_protocol::account::auth::AuthScheme;
137
138    use super::*;
139
140    fn approver(seed: u32) -> Approver {
141        Approver::new(PublicKeyCommitment::from(Word::from([seed; 4])), AuthScheme::EcdsaK256Keccak)
142    }
143
144    #[test]
145    fn rejects_zero_threshold() {
146        let err = ApproverSet::new(vec![approver(1)], 0).unwrap_err();
147        assert!(err.to_string().contains("threshold must be at least 1"));
148    }
149
150    #[test]
151    fn rejects_threshold_above_approver_count() {
152        let err = ApproverSet::new(vec![approver(1)], 2).unwrap_err();
153        assert!(err.to_string().contains("threshold cannot be greater than number of approvers"));
154    }
155
156    #[test]
157    fn rejects_approver_count_above_max() {
158        let approvers: Vec<_> = (0..=u32::from(ApproverSet::MAX_APPROVERS)).map(approver).collect();
159        let err = ApproverSet::new(approvers, 1).unwrap_err();
160        assert!(err.to_string().contains("number of approvers cannot be greater than 64"));
161    }
162
163    #[test]
164    fn accepts_approver_count_at_max() {
165        let max_approvers = u32::from(ApproverSet::MAX_APPROVERS);
166        let approvers: Vec<_> = (0..max_approvers).map(approver).collect();
167        let set = ApproverSet::new(approvers, max_approvers).unwrap();
168        assert_eq!(set.approvers().len(), max_approvers as usize);
169    }
170
171    #[test]
172    fn rejects_duplicate_approvers() {
173        let err = ApproverSet::new(vec![approver(1), approver(1)], 2).unwrap_err();
174        assert!(err.to_string().contains("duplicate approver public keys are not allowed"));
175    }
176
177    #[test]
178    fn accepts_valid_set() {
179        let set = ApproverSet::new(vec![approver(1), approver(2)], 2).unwrap();
180        assert_eq!(set.approvers().len(), 2);
181        assert_eq!(set.threshold().get(), 2);
182    }
183}