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