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}