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