Skip to main content

miden_standards/account/auth/
approver.rs

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