Skip to main content

miden_tx/auth/
tx_authenticator.rs

1use alloc::boxed::Box;
2use alloc::collections::BTreeMap;
3use alloc::string::ToString;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6
7use miden_processor::FutureMaybeSend;
8use miden_protocol::account::auth::{AuthSecretKey, PublicKey, PublicKeyCommitment, Signature};
9use miden_protocol::crypto::SequentialCommit;
10use miden_protocol::transaction::TransactionSummary;
11use miden_protocol::{Felt, Hasher, Word};
12
13use crate::errors::AuthenticationError;
14use crate::utils::serde::{
15    ByteReader,
16    ByteWriter,
17    Deserializable,
18    DeserializationError,
19    Serializable,
20};
21
22// SIGNATURE DATA
23// ================================================================================================
24
25/// Data types on which a signature can be requested.
26///
27/// It supports three modes:
28/// - `TransactionSummary`: Structured transaction summary, recommended for authenticating
29///   transactions.
30/// - `Arbitrary`: Arbitrary payload provided by the application. It is up to the authenticator to
31///   display it appropriately.
32/// - `Blind`: The underlying data is not meant to be displayed in a human-readable format. It must
33///   be a cryptographic commitment to some data.
34#[derive(Debug, Clone)]
35pub enum SigningInputs {
36    TransactionSummary(Box<TransactionSummary>),
37    Arbitrary(Vec<Felt>),
38    Blind(Word),
39}
40
41impl SequentialCommit for SigningInputs {
42    type Commitment = Word;
43
44    fn to_elements(&self) -> Vec<Felt> {
45        match self {
46            SigningInputs::TransactionSummary(tx_summary) => tx_summary.as_ref().to_elements(),
47            SigningInputs::Arbitrary(elements) => elements.clone(),
48            SigningInputs::Blind(word) => word.as_elements().to_vec(),
49        }
50    }
51
52    fn to_commitment(&self) -> Self::Commitment {
53        match self {
54            // `TransactionSummary` knows how to derive a commitment to itself.
55            SigningInputs::TransactionSummary(tx_summary) => tx_summary.as_ref().to_commitment(),
56            // use the default implementation.
57            SigningInputs::Arbitrary(elements) => Hasher::hash_elements(elements),
58            // `Blind` is assumed to already be a commitment.
59            SigningInputs::Blind(word) => *word,
60        }
61    }
62}
63
64/// Convenience methods for [SigningInputs].
65impl SigningInputs {
66    /// Computes the commitment to [SigningInputs].
67    pub fn to_commitment(&self) -> Word {
68        <Self as SequentialCommit>::to_commitment(self)
69    }
70
71    /// Returns a representation of the [SigningInputs] as a sequence of field elements.
72    pub fn to_elements(&self) -> Vec<Felt> {
73        <Self as SequentialCommit>::to_elements(self)
74    }
75}
76
77// SERIALIZATION
78// ================================================================================================
79
80impl Serializable for SigningInputs {
81    fn write_into<W: ByteWriter>(&self, target: &mut W) {
82        match self {
83            SigningInputs::TransactionSummary(tx_summary) => {
84                target.write_u8(0);
85                tx_summary.as_ref().write_into(target);
86            },
87            SigningInputs::Arbitrary(elements) => {
88                target.write_u8(1);
89                elements.write_into(target);
90            },
91            SigningInputs::Blind(word) => {
92                target.write_u8(2);
93                word.write_into(target);
94            },
95        }
96    }
97}
98
99impl Deserializable for SigningInputs {
100    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
101        let discriminant = source.read_u8()?;
102        match discriminant {
103            0 => {
104                let tx_summary: TransactionSummary = source.read()?;
105                Ok(SigningInputs::TransactionSummary(Box::new(tx_summary)))
106            },
107            1 => {
108                let elements: Vec<Felt> = source.read()?;
109                Ok(SigningInputs::Arbitrary(elements))
110            },
111            2 => {
112                let word: Word = source.read()?;
113                Ok(SigningInputs::Blind(word))
114            },
115            other => Err(DeserializationError::InvalidValue(format!(
116                "invalid SigningInputs variant: {other}"
117            ))),
118        }
119    }
120}
121
122// TRANSACTION AUTHENTICATOR
123// ================================================================================================
124
125/// Defines an authenticator for transactions.
126///
127/// The main purpose of the authenticator is to generate signatures for a given message against
128/// a key managed by the authenticator. That is, the authenticator maintains a set of public-
129/// private key pairs, and can be requested to generate signatures against any of the managed keys.
130///
131/// The public keys are defined by [PublicKeyCommitment]'s which are the hashes of the actual
132/// public keys.
133pub trait TransactionAuthenticator {
134    /// Retrieves a signature for a specific message as a list of [Felt].
135    ///
136    /// The request is initiated by the VM as a consequence of the SigToStack advice
137    /// injector.
138    ///
139    /// - `pub_key_commitment`: the hash of the public key used for signature generation.
140    /// - `signing_inputs`: description of the message to be singed. The inputs could contain
141    ///   arbitrary data or a [TransactionSummary] which would describe the changes made to the
142    ///   account up to the point of calling `get_signature()`. This allows the authenticator to
143    ///   review any alterations to the account prior to signing. It should not be directly used in
144    ///   the signature computation.
145    fn get_signature(
146        &self,
147        pub_key_commitment: PublicKeyCommitment,
148        signing_inputs: &SigningInputs,
149    ) -> impl FutureMaybeSend<Result<Signature, AuthenticationError>>;
150
151    /// Retrieves a public key for a specific public key commitment.
152    fn get_public_key(
153        &self,
154        pub_key_commitment: PublicKeyCommitment,
155    ) -> impl FutureMaybeSend<Option<Arc<PublicKey>>>;
156}
157
158/// A placeholder type for the generic trait bound of `TransactionAuthenticator<'_,'_,_,T>`
159/// when we do not want to provide one, but must provide the `T` in `Option<T>`.
160///
161/// Note: Asserts when `get_signature` is called.
162#[derive(Debug, Clone, Copy)]
163pub struct UnreachableAuth {
164    // ensure the type cannot be instantiated
165    _protect: core::marker::PhantomData<u8>,
166}
167
168impl TransactionAuthenticator for UnreachableAuth {
169    #[allow(clippy::manual_async_fn)]
170    fn get_signature(
171        &self,
172        _pub_key_commitment: PublicKeyCommitment,
173        _signing_inputs: &SigningInputs,
174    ) -> impl FutureMaybeSend<Result<Signature, AuthenticationError>> {
175        async { unreachable!("Type `UnreachableAuth` must not be instantiated") }
176    }
177
178    fn get_public_key(
179        &self,
180        _pub_key_commitment: PublicKeyCommitment,
181    ) -> impl FutureMaybeSend<Option<Arc<PublicKey>>> {
182        async { unreachable!("Type `UnreachableAuth` must not be instantiated") }
183    }
184}
185
186// BASIC AUTHENTICATOR
187// ================================================================================================
188
189/// Represents a signer for [AuthSecretKey] keys.
190#[derive(Clone, Debug)]
191pub struct BasicAuthenticator {
192    /// pub_key |-> (secret_key, public_key) mapping
193    keys: BTreeMap<PublicKeyCommitment, (AuthSecretKey, Arc<PublicKey>)>,
194}
195
196impl BasicAuthenticator {
197    pub fn new(keys: &[AuthSecretKey]) -> Self {
198        let mut key_map = BTreeMap::new();
199        for secret_key in keys {
200            let pub_key = secret_key.public_key();
201            key_map.insert(pub_key.to_commitment(), (secret_key.clone(), pub_key.into()));
202        }
203
204        BasicAuthenticator { keys: key_map }
205    }
206
207    pub fn from_key_pairs(keys: &[(AuthSecretKey, PublicKey)]) -> Self {
208        let mut key_map = BTreeMap::new();
209        for (secret_key, public_key) in keys {
210            key_map.insert(
211                public_key.to_commitment(),
212                (secret_key.clone(), public_key.clone().into()),
213            );
214        }
215
216        BasicAuthenticator { keys: key_map }
217    }
218
219    /// Returns a reference to the keys map.
220    ///
221    /// Map keys represent the public key commitments, and values represent the (secret_key,
222    /// public_key) pair that the authenticator would use to sign messages.
223    pub fn keys(&self) -> &BTreeMap<PublicKeyCommitment, (AuthSecretKey, Arc<PublicKey>)> {
224        &self.keys
225    }
226}
227
228impl TransactionAuthenticator for BasicAuthenticator {
229    /// Gets a signature over a message, given a public key commitment.
230    ///
231    /// The key should be included in the `keys` map and should be a variant of [AuthSecretKey].
232    ///
233    /// # Errors
234    /// If the public key is not contained in the `keys` map,
235    /// [`AuthenticationError::UnknownPublicKey`] is returned.
236    fn get_signature(
237        &self,
238        pub_key_commitment: PublicKeyCommitment,
239        signing_inputs: &SigningInputs,
240    ) -> impl FutureMaybeSend<Result<Signature, AuthenticationError>> {
241        let message = signing_inputs.to_commitment();
242
243        async move {
244            match self.keys.get(&pub_key_commitment) {
245                Some((auth_key, _)) => Ok(auth_key.sign(message)),
246                None => Err(AuthenticationError::UnknownPublicKey(pub_key_commitment)),
247            }
248        }
249    }
250
251    /// Returns the public key associated with the given public key commitment.
252    ///
253    /// If the public key commitment is not contained in the `keys` map, `None` is returned.
254    fn get_public_key(
255        &self,
256        pub_key_commitment: PublicKeyCommitment,
257    ) -> impl FutureMaybeSend<Option<Arc<PublicKey>>> {
258        async move { self.keys.get(&pub_key_commitment).map(|(_, pub_key)| pub_key.clone()) }
259    }
260}
261
262// EMPTY AUTHENTICATOR
263// ================================================================================================
264
265impl TransactionAuthenticator for () {
266    #[allow(clippy::manual_async_fn)]
267    fn get_signature(
268        &self,
269        _pub_key_commitment: PublicKeyCommitment,
270        _signing_inputs: &SigningInputs,
271    ) -> impl FutureMaybeSend<Result<Signature, AuthenticationError>> {
272        async {
273            Err(AuthenticationError::RejectedSignature(
274                "default authenticator cannot provide signatures".to_string(),
275            ))
276        }
277    }
278
279    fn get_public_key(
280        &self,
281        _pub_key_commitment: PublicKeyCommitment,
282    ) -> impl FutureMaybeSend<Option<Arc<PublicKey>>> {
283        async { None }
284    }
285}
286
287// TESTS
288// ================================================================================================
289
290#[cfg(test)]
291mod test {
292    use miden_protocol::account::auth::AuthSecretKey;
293    use miden_protocol::utils::serde::{Deserializable, Serializable};
294    use miden_protocol::{Felt, Word};
295
296    use super::SigningInputs;
297
298    #[test]
299    fn serialize_auth_key() {
300        let auth_key = AuthSecretKey::new_falcon512_poseidon2();
301        let serialized = auth_key.to_bytes();
302        let deserialized = AuthSecretKey::read_from_bytes(&serialized).unwrap();
303
304        assert_eq!(auth_key, deserialized);
305    }
306
307    #[test]
308    fn serialize_deserialize_signing_inputs_arbitrary() {
309        let elements = vec![
310            Felt::new(0),
311            Felt::new(1),
312            Felt::new(2),
313            Felt::new(3),
314            Felt::new(4),
315            Felt::new(5),
316            Felt::new(6),
317            Felt::new(7),
318        ];
319        let inputs = SigningInputs::Arbitrary(elements.clone());
320        let bytes = inputs.to_bytes();
321        let decoded = SigningInputs::read_from_bytes(&bytes).unwrap();
322
323        match decoded {
324            SigningInputs::Arbitrary(decoded_elements) => {
325                assert_eq!(decoded_elements, elements);
326            },
327            _ => panic!("expected Arbitrary variant"),
328        }
329    }
330
331    #[test]
332    fn serialize_deserialize_signing_inputs_blind() {
333        let word = Word::from([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]);
334        let inputs = SigningInputs::Blind(word);
335        let bytes = inputs.to_bytes();
336        let decoded = SigningInputs::read_from_bytes(&bytes).unwrap();
337
338        match decoded {
339            SigningInputs::Blind(w) => {
340                assert_eq!(w, word);
341            },
342            _ => panic!("expected Blind variant"),
343        }
344    }
345}