iota_types/
move_authenticator.rs

1// Copyright (c) 2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::HashSet,
6    hash::{Hash, Hasher},
7};
8
9use enum_dispatch::enum_dispatch;
10use fastcrypto::{error::FastCryptoError, traits::ToFromBytes};
11use iota_protocol_config::ProtocolConfig;
12use iota_sdk_types::crypto::IntentMessage;
13use once_cell::sync::OnceCell;
14use serde::{Deserialize, Serialize};
15
16use crate::{
17    base_types::{IotaAddress, ObjectID, ObjectRef, SequenceNumber, TypeTag},
18    crypto::{SignatureScheme, default_hash},
19    digests::{MoveAuthenticatorDigest, ObjectDigest},
20    error::{IotaError, IotaResult, UserInputError, UserInputResult},
21    signature::{AuthenticatorTrait, VerifyParams},
22    transaction::{CallArg, CallArgExt, InputObjectKind, SharedObjectRef},
23};
24
25/// MoveAuthenticator is a GenericSignature variant that enables a new
26/// method of authentication through Move code.
27/// This function represents the data received by the Move authenticate function
28/// during the Account Abstraction authentication flow.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct MoveAuthenticator {
31    #[serde(flatten)]
32    pub(crate) inner: MoveAuthenticatorInner,
33    /// A bytes representation of [struct MoveAuthenticator]. This helps with
34    /// implementing trait [AsRef](core::convert::AsRef).
35    #[serde(skip)]
36    bytes: OnceCell<Vec<u8>>,
37}
38
39impl MoveAuthenticator {
40    /// Creates a new MoveAuthenticator of version 1.
41    pub fn new_v1(
42        call_args: Vec<CallArg>,
43        type_arguments: Vec<TypeTag>,
44        object_to_authenticate: CallArg,
45    ) -> Self {
46        Self {
47            inner: MoveAuthenticatorInner::new_v1(
48                call_args,
49                type_arguments,
50                object_to_authenticate,
51            ),
52            bytes: OnceCell::new(),
53        }
54    }
55
56    /// Constructs a `MoveAuthenticator` from a deserialized
57    /// [`MoveAuthenticatorInner`].
58    pub(crate) fn from_inner(inner: MoveAuthenticatorInner) -> Self {
59        Self {
60            inner,
61            bytes: OnceCell::new(),
62        }
63    }
64
65    /// Computes the digest of the MoveAuthenticator.
66    pub fn digest(&self) -> MoveAuthenticatorDigest {
67        MoveAuthenticatorDigest::new(default_hash(self))
68    }
69
70    /// Returns the version of the MoveAuthenticator.
71    pub fn version(&self) -> u64 {
72        self.inner.version()
73    }
74
75    /// Returns the address of the MoveAuthenticator.
76    pub fn address(&self) -> IotaResult<IotaAddress> {
77        self.inner.address()
78    }
79
80    /// Returns the call arguments of the MoveAuthenticator.
81    pub fn call_args(&self) -> &Vec<CallArg> {
82        self.inner.call_args()
83    }
84
85    /// Returns the type arguments of the MoveAuthenticator.
86    pub fn type_arguments(&self) -> &Vec<TypeTag> {
87        self.inner.type_arguments()
88    }
89
90    /// Returns the object to authenticate of the MoveAuthenticator.
91    pub fn object_to_authenticate(&self) -> &CallArg {
92        self.inner.object_to_authenticate()
93    }
94
95    /// Returns the components of the object to authenticate.
96    pub fn object_to_authenticate_components(
97        &self,
98    ) -> UserInputResult<(ObjectID, Option<SequenceNumber>, Option<ObjectDigest>)> {
99        self.inner.object_to_authenticate_components()
100    }
101
102    /// Returns all input objects used by the MoveAuthenticator,
103    /// including those from the object to authenticate.
104    pub fn input_objects(&self) -> Vec<InputObjectKind> {
105        self.inner.input_objects()
106    }
107
108    /// Returns all receiving objects used by the MoveAuthenticator.
109    pub fn receiving_objects(&self) -> Vec<ObjectRef> {
110        self.inner.receiving_objects()
111    }
112
113    /// Returns all shared input objects used by the MoveAuthenticator,
114    /// including those from the object to authenticate.
115    pub fn shared_objects(&self) -> Vec<SharedObjectRef> {
116        self.inner.shared_objects()
117    }
118
119    /// Validity check for MoveAuthenticator.
120    pub fn validity_check(&self, config: &ProtocolConfig) -> UserInputResult {
121        self.inner.validity_check(config)
122    }
123}
124
125impl AuthenticatorTrait for MoveAuthenticator {
126    // This function accepts all inputs, as signature verification is performed
127    // later on the Move side.
128    fn verify_claims<T>(
129        &self,
130        value: &IntentMessage<T>,
131        author: IotaAddress,
132        aux_verify_data: &VerifyParams,
133    ) -> IotaResult
134    where
135        T: Serialize,
136    {
137        self.inner.verify_claims(value, author, aux_verify_data)
138    }
139}
140
141/// Necessary trait for
142/// [SenderSignerData](crate::transaction::SenderSignedData). This trait is
143/// implemented only for MoveAuthenticator and not for specific versions of
144/// MoveAuthenticator (e.g., MoveAuthenticatorV1) because the custom
145/// serialization/deserialization signature logic is defined on the
146/// MoveAuthenticator level.
147impl Hash for MoveAuthenticator {
148    fn hash<H: Hasher>(&self, state: &mut H) {
149        self.as_ref().hash(state);
150    }
151}
152
153/// Necessary trait for
154/// [GenericSignature](crate::signature::GenericSignature). This trait is
155/// implemented only for MoveAuthenticator and not for specific versions of
156/// MoveAuthenticator (e.g., MoveAuthenticatorV1) because the custom
157/// serialization/deserialization signature logic is defined on the
158/// MoveAuthenticator level.
159impl ToFromBytes for MoveAuthenticator {
160    fn from_bytes(bytes: &[u8]) -> Result<Self, FastCryptoError> {
161        // The first byte matches the flag of MoveAuthenticator.
162        if bytes.first().ok_or(FastCryptoError::InvalidInput)?
163            != &SignatureScheme::MoveAuthenticator.flag()
164        {
165            return Err(FastCryptoError::InvalidInput);
166        }
167
168        let inner: MoveAuthenticatorInner =
169            bcs::from_bytes(&bytes[1..]).map_err(|_| FastCryptoError::InvalidSignature)?;
170        Ok(Self {
171            inner,
172            bytes: OnceCell::new(),
173        })
174    }
175}
176
177/// Necessary trait for
178/// [GenericSignature](crate::signature::GenericSignature). This trait is
179/// implemented only for MoveAuthenticator and not for specific versions of
180/// MoveAuthenticator (e.g., MoveAuthenticatorV1) because the custom
181/// serialization/deserialization signature logic is defined on the
182/// MoveAuthenticator level.
183impl AsRef<[u8]> for MoveAuthenticator {
184    fn as_ref(&self) -> &[u8] {
185        self.bytes.get_or_init(|| {
186            let as_bytes = bcs::to_bytes(&self.inner).expect("BCS serialization should not fail");
187            let mut bytes = Vec::with_capacity(1 + as_bytes.len());
188            bytes.push(SignatureScheme::MoveAuthenticator.flag());
189            bytes.extend_from_slice(as_bytes.as_slice());
190            bytes
191        })
192    }
193}
194
195/// Necessary trait for
196/// [SenderSignerData](crate::transaction::SenderSignedData). This trait is
197/// implemented only for MoveAuthenticator and not for specific versions of
198/// MoveAuthenticator (e.g., MoveAuthenticatorV1) because the custom
199/// serialization/deserialization signature logic is defined on the
200/// MoveAuthenticator level.
201impl PartialEq for MoveAuthenticator {
202    fn eq(&self, other: &Self) -> bool {
203        self.as_ref() == other.as_ref()
204    }
205}
206
207/// Necessary trait for
208/// [SenderSignerData](crate::transaction::SenderSignedData). This trait is
209/// implemented only for MoveAuthenticator and not for specific versions of
210/// MoveAuthenticator (e.g., MoveAuthenticatorV1) because the custom
211/// serialization/deserialization signature logic is defined at the
212/// MoveAuthenticator level.
213impl Eq for MoveAuthenticator {}
214
215/// MoveAuthenticatorInner is an enum that represents the different versions
216/// of MoveAuthenticator.
217#[enum_dispatch(AuthenticatorTrait)]
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub enum MoveAuthenticatorInner {
220    V1(MoveAuthenticatorV1),
221}
222
223impl MoveAuthenticatorInner {
224    pub fn new_v1(
225        call_args: Vec<CallArg>,
226        type_arguments: Vec<TypeTag>,
227        object_to_authenticate: CallArg,
228    ) -> Self {
229        MoveAuthenticatorInner::V1(MoveAuthenticatorV1::new(
230            call_args,
231            type_arguments,
232            object_to_authenticate,
233        ))
234    }
235
236    pub fn version(&self) -> u64 {
237        match self {
238            MoveAuthenticatorInner::V1(_) => 1,
239        }
240    }
241
242    pub fn address(&self) -> IotaResult<IotaAddress> {
243        match self {
244            MoveAuthenticatorInner::V1(v1) => v1.address(),
245        }
246    }
247
248    pub fn call_args(&self) -> &Vec<CallArg> {
249        match self {
250            MoveAuthenticatorInner::V1(v1) => v1.call_args(),
251        }
252    }
253
254    pub fn type_arguments(&self) -> &Vec<TypeTag> {
255        match self {
256            MoveAuthenticatorInner::V1(v1) => v1.type_arguments(),
257        }
258    }
259
260    pub fn object_to_authenticate(&self) -> &CallArg {
261        match self {
262            MoveAuthenticatorInner::V1(v1) => v1.object_to_authenticate(),
263        }
264    }
265
266    pub fn object_to_authenticate_components(
267        &self,
268    ) -> UserInputResult<(ObjectID, Option<SequenceNumber>, Option<ObjectDigest>)> {
269        match self {
270            MoveAuthenticatorInner::V1(v1) => v1.object_to_authenticate_components(),
271        }
272    }
273
274    pub fn input_objects(&self) -> Vec<InputObjectKind> {
275        match self {
276            MoveAuthenticatorInner::V1(v1) => v1.input_objects(),
277        }
278    }
279
280    pub fn receiving_objects(&self) -> Vec<ObjectRef> {
281        match self {
282            MoveAuthenticatorInner::V1(v1) => v1.receiving_objects(),
283        }
284    }
285
286    pub fn shared_objects(&self) -> Vec<SharedObjectRef> {
287        match self {
288            MoveAuthenticatorInner::V1(v1) => v1.shared_objects(),
289        }
290    }
291
292    pub fn validity_check(&self, config: &ProtocolConfig) -> UserInputResult {
293        match self {
294            MoveAuthenticatorInner::V1(v1) => v1.validity_check(config),
295        }
296    }
297}
298
299/// MoveAuthenticatorV1 is the first version of MoveAuthenticator.
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct MoveAuthenticatorV1 {
302    /// Input objects or primitive values
303    call_args: Vec<CallArg>,
304    /// Type arguments for the Move authenticate function
305    type_arguments: Vec<TypeTag>,
306    /// The object that is authenticated. Represents the account being the
307    /// sender of the transaction.
308    object_to_authenticate: CallArg,
309}
310
311impl MoveAuthenticatorV1 {
312    pub fn new(
313        call_args: Vec<CallArg>,
314        type_arguments: Vec<TypeTag>,
315        object_to_authenticate: CallArg,
316    ) -> Self {
317        Self {
318            call_args,
319            type_arguments,
320            object_to_authenticate,
321        }
322    }
323
324    /// Returns the address of the MoveAuthenticatorV1, which is the object ID
325    /// of the object to authenticate.
326    pub fn address(&self) -> IotaResult<IotaAddress> {
327        let (id, _, _) = self.object_to_authenticate_components()?;
328        Ok(IotaAddress::from(id))
329    }
330
331    pub fn call_args(&self) -> &Vec<CallArg> {
332        &self.call_args
333    }
334
335    pub fn type_arguments(&self) -> &Vec<TypeTag> {
336        &self.type_arguments
337    }
338
339    pub fn object_to_authenticate(&self) -> &CallArg {
340        &self.object_to_authenticate
341    }
342
343    pub fn object_to_authenticate_components(
344        &self,
345    ) -> UserInputResult<(ObjectID, Option<SequenceNumber>, Option<ObjectDigest>)> {
346        Ok(match self.object_to_authenticate() {
347            CallArg::Pure(_) => {
348                return Err(UserInputError::Unsupported(
349                    "MoveAuthenticatorV1 cannot authenticate pure inputs".to_string(),
350                ));
351            }
352            CallArg::ImmutableOrOwned(object_ref) => (
353                object_ref.object_id,
354                Some(object_ref.version),
355                Some(object_ref.digest),
356            ),
357            CallArg::Shared(SharedObjectRef {
358                object_id, mutable, ..
359            }) => {
360                if *mutable {
361                    return Err(UserInputError::Unsupported(
362                        "MoveAuthenticatorV1 cannot authenticate mutable shared objects"
363                            .to_string(),
364                    ));
365                }
366
367                (*object_id, None, None)
368            }
369            CallArg::Receiving(_) => {
370                return Err(UserInputError::Unsupported(
371                    "MoveAuthenticator cannot authenticate receiving objects".to_string(),
372                ));
373            }
374            _ => unimplemented!("a new CallArg enum variant was added and needs to be handled"),
375        })
376    }
377
378    /// Returns all input objects used by the MoveAuthenticatorV1,
379    /// including those from the object to authenticate.
380    pub fn input_objects(&self) -> Vec<InputObjectKind> {
381        self.call_args
382            .iter()
383            .filter_map(|arg| arg.input_object_kind())
384            .chain(self.object_to_authenticate().input_object_kind())
385            .collect::<Vec<_>>()
386    }
387
388    pub fn receiving_objects(&self) -> Vec<ObjectRef> {
389        self.call_args
390            .iter()
391            .filter_map(|arg| arg.as_receiving_opt().copied())
392            .collect()
393    }
394
395    /// Returns all shared input objects used by the MoveAuthenticatorV1,
396    /// including those from the object to authenticate.
397    pub fn shared_objects(&self) -> Vec<SharedObjectRef> {
398        self.call_args
399            .iter()
400            .filter_map(|arg| arg.as_shared_opt())
401            .chain(self.object_to_authenticate().as_shared_opt())
402            .cloned()
403            .collect()
404    }
405
406    /// Validity check for MoveAuthenticatorV1.
407    pub fn validity_check(&self, config: &ProtocolConfig) -> UserInputResult {
408        // Check that the object to authenticate is valid.
409        self.object_to_authenticate_components()?;
410
411        // Inputs validity check.
412        //
413        // `validity_check` is not called for `object_to_authenticate` because it is
414        // already validated with a dedicated function.
415
416        // `ProtocolConfig::max_function_parameters` is used to check the call arguments
417        // because MoveAuthenticatorV1 is considered as a simple programmable call to a
418        // Move function.
419        //
420        // The limit includes the object to authenticate, the auth context and the tx
421        // context, so we subtract 3 here.
422        let max_args = (config.max_function_parameters() - 3) as usize;
423        fp_ensure!(
424            self.call_args().len() < max_args,
425            UserInputError::SizeLimitExceeded {
426                limit: "maximum arguments in MoveAuthenticatorV1".to_string(),
427                value: max_args.to_string()
428            }
429        );
430
431        fp_ensure!(
432            self.receiving_objects().is_empty(),
433            UserInputError::Unsupported(
434                "MoveAuthenticatorV1 cannot have receiving objects as input".to_string(),
435            )
436        );
437
438        let mut used = HashSet::new();
439        fp_ensure!(
440            self.input_objects()
441                .iter()
442                .all(|o| used.insert(o.object_id())),
443            UserInputError::DuplicateObjectRefInput
444        );
445
446        self.call_args()
447            .iter()
448            .try_for_each(|obj| obj.validity_check(config))?;
449
450        // Type arguments validity check.
451        //
452        // Each type argument is checked for validity in the same way as it is done for
453        // `ProgrammableMoveCall`.
454        let mut type_arguments_count = 0;
455        self.type_arguments().iter().try_for_each(|type_arg| {
456            crate::transaction::type_tag_validity_check(type_arg, config, &mut type_arguments_count)
457        })?;
458
459        Ok(())
460    }
461}
462
463impl AuthenticatorTrait for MoveAuthenticatorV1 {
464    // This function accepts all inputs, as signature verification is performed
465    // later on the Move side.
466    fn verify_claims<T>(
467        &self,
468        _value: &IntentMessage<T>,
469        author: IotaAddress,
470        _aux_verify_data: &VerifyParams,
471    ) -> IotaResult
472    where
473        T: Serialize,
474    {
475        if author != self.address()? {
476            return Err(IotaError::InvalidSignature {
477                error: "Invalid author".to_string(),
478            });
479        };
480
481        Ok(())
482    }
483}
484
485#[cfg(test)]
486mod tests {
487    use fastcrypto::traits::ToFromBytes;
488
489    use super::*;
490    use crate::{
491        base_types::{ObjectID, ObjectRef, SequenceNumber},
492        digests::ObjectDigest,
493        transaction::CallArg,
494    };
495
496    fn make_simple_authenticator() -> MoveAuthenticator {
497        let object_to_authenticate = CallArg::ImmutableOrOwned(ObjectRef {
498            object_id: ObjectID::ZERO,
499            version: SequenceNumber::default(),
500            digest: ObjectDigest::MIN,
501        });
502        MoveAuthenticator::new_v1(vec![], vec![], object_to_authenticate)
503    }
504
505    #[test]
506    fn round_trip() {
507        let auth = make_simple_authenticator();
508        let bytes = auth.as_ref().to_vec();
509        let decoded = MoveAuthenticator::from_bytes(&bytes).expect("round-trip should succeed");
510        assert_eq!(auth, decoded);
511    }
512
513    #[test]
514    fn as_ref_starts_with_flag_byte() {
515        let auth = make_simple_authenticator();
516        let bytes = auth.as_ref();
517        assert_eq!(bytes[0], SignatureScheme::MoveAuthenticator.flag());
518    }
519
520    #[test]
521    fn as_ref_is_cached() {
522        let auth = make_simple_authenticator();
523        let bytes1 = auth.as_ref();
524        let bytes2 = auth.as_ref();
525        assert!(std::ptr::eq(bytes1.as_ptr(), bytes2.as_ptr()));
526    }
527
528    #[test]
529    fn from_bytes_rejects_wrong_flag() {
530        let auth = make_simple_authenticator();
531        let mut bytes = auth.as_ref().to_vec();
532        bytes[0] = SignatureScheme::ED25519.flag();
533        assert!(MoveAuthenticator::from_bytes(&bytes).is_err());
534    }
535
536    #[test]
537    fn from_bytes_rejects_empty_input() {
538        assert!(MoveAuthenticator::from_bytes(&[]).is_err());
539    }
540
541    #[test]
542    fn from_bytes_rejects_flag_only() {
543        let flag = SignatureScheme::MoveAuthenticator.flag();
544        assert!(MoveAuthenticator::from_bytes(&[flag]).is_err());
545    }
546
547    // ---- Signable / SignableBytes round-trip tests ----
548
549    use crate::crypto::{Signable, SignableBytes};
550
551    /// Helper: produce the signable bytes for a MoveAuthenticator (the
552    /// `"MoveAuthenticator::" ++ BCS(inner)` format).
553    fn signable_bytes(auth: &MoveAuthenticator) -> Vec<u8> {
554        let mut buf = Vec::new();
555        auth.write(&mut buf);
556        buf
557    }
558
559    #[test]
560    fn signable_round_trip() {
561        let auth = make_simple_authenticator();
562        let bytes = signable_bytes(&auth);
563        let decoded = MoveAuthenticator::from_signable_bytes(&bytes)
564            .expect("round-trip via signable bytes should succeed");
565        assert_eq!(auth, decoded);
566    }
567
568    #[test]
569    fn signable_bytes_start_with_name_tag() {
570        let auth = make_simple_authenticator();
571        let bytes = signable_bytes(&auth);
572        let tag = b"MoveAuthenticator::";
573        assert!(
574            bytes.starts_with(tag),
575            "signable bytes must start with the hardcoded name tag"
576        );
577    }
578
579    #[test]
580    fn signable_bytes_payload_is_bcs_of_inner() {
581        let auth = make_simple_authenticator();
582        let bytes = signable_bytes(&auth);
583        let tag_len = "MoveAuthenticator::".len();
584        let payload = &bytes[tag_len..];
585        let expected_bcs = bcs::to_bytes(&auth.inner).expect("BCS serialization should not fail");
586        assert_eq!(payload, expected_bcs.as_slice());
587    }
588
589    #[test]
590    fn from_signable_bytes_rejects_empty() {
591        assert!(MoveAuthenticator::from_signable_bytes(&[]).is_err());
592    }
593
594    #[test]
595    fn from_signable_bytes_rejects_short_input() {
596        // Shorter than the name tag — should fail, not panic.
597        assert!(MoveAuthenticator::from_signable_bytes(b"Move").is_err());
598    }
599
600    #[test]
601    fn from_signable_bytes_rejects_tag_only() {
602        // Exact tag with no BCS payload.
603        assert!(MoveAuthenticator::from_signable_bytes(b"MoveAuthenticator::").is_err());
604    }
605
606    #[test]
607    fn from_signable_bytes_rejects_corrupt_payload() {
608        let auth = make_simple_authenticator();
609        let mut bytes = signable_bytes(&auth);
610        // Truncate the BCS payload so it is incomplete.
611        let tag_len = "MoveAuthenticator::".len();
612        bytes.truncate(tag_len + 1);
613        assert!(MoveAuthenticator::from_signable_bytes(&bytes).is_err());
614    }
615
616    #[test]
617    fn digest_is_stable() {
618        let auth = make_simple_authenticator();
619        let d1 = auth.digest();
620        let d2 = auth.digest();
621        assert_eq!(d1, d2, "digest must be deterministic");
622    }
623}