iota_types/auth_context/
mod.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4mod fields_v1;
5
6pub use fields_v1::*;
7use move_binary_format::{CompiledModule, file_format::SignatureToken};
8use move_bytecode_utils::resolve_struct;
9use move_core_types::{
10    account_address::AccountAddress, ident_str, identifier::IdentStr, language_storage::StructTag,
11};
12use serde::Serialize;
13
14use crate::{
15    IOTA_FRAMEWORK_ADDRESS, digests::MoveAuthenticatorDigest, transaction::ProgrammableTransaction,
16};
17
18pub const AUTH_CONTEXT_MODULE_NAME: &IdentStr = ident_str!("auth_context");
19pub const AUTH_CONTEXT_STRUCT_NAME: &IdentStr = ident_str!("AuthContext");
20
21/// `AuthContext` provides a lightweight execution context used during the
22/// authentication phase of a transaction.
23///
24/// It allows authenticator functions to:
25/// - Inspect the programmable transaction block (PTB) inputs and commands
26/// - Perform function-level permission checks
27/// - Support OTP, time-locked auth, or regulatory rule enforcement
28///
29/// This struct is **immutable** during the auth phase and must not allow
30/// mutation of state or access to storage beyond what is declared.
31///
32/// It is guaranteed to be available to all smart accounts implementing a
33/// custom authenticator function.
34///
35/// Typical use:
36/// ```move
37/// public fun authenticate(account: &Account, signature: &vector<u8>, auth_ctx: &AuthContext, , ctx: &TxContext) {
38///     assert!(ed25519::ed25519_verify(signature, &account.pub_key, ctx.digest()), EEd25519VerificationFailed);
39///     
40///     assert!(is_authorized(&extract_function_key(&auth_ctx)), EUnauthorized);
41///     ...
42/// }
43/// ```
44// Conceptually similar to `TxContext`, but designed specifically for use in the authentication
45// flow.
46#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
47pub struct AuthContext {
48    /// The digest of the MoveAuthenticator
49    auth_digest: MoveAuthenticatorDigest,
50    /// The authentication input objects or primitive values
51    tx_inputs: Vec<MoveCallArg>,
52    /// The authentication commands to be executed sequentially.
53    tx_commands: Vec<MoveCommand>,
54    /// The BCS-serialized `TransactionData` bytes.
55    tx_data_bytes: Vec<u8>,
56}
57
58impl AuthContext {
59    pub fn new_from_components(
60        auth_digest: MoveAuthenticatorDigest,
61        ptb: &ProgrammableTransaction,
62        tx_data_bytes: Vec<u8>,
63    ) -> Self {
64        Self {
65            auth_digest,
66            tx_inputs: ptb.inputs.iter().map(MoveCallArg::from).collect(),
67            tx_commands: ptb.commands.iter().map(MoveCommand::from).collect(),
68            tx_data_bytes,
69        }
70    }
71
72    pub fn new_for_testing() -> Self {
73        Self {
74            auth_digest: MoveAuthenticatorDigest::default(),
75            tx_inputs: Vec::new(),
76            tx_commands: Vec::new(),
77            tx_data_bytes: Vec::new(),
78        }
79    }
80
81    pub fn digest(&self) -> &MoveAuthenticatorDigest {
82        &self.auth_digest
83    }
84
85    pub fn tx_inputs(&self) -> &Vec<MoveCallArg> {
86        &self.tx_inputs
87    }
88
89    pub fn tx_commands(&self) -> &Vec<MoveCommand> {
90        &self.tx_commands
91    }
92
93    pub fn tx_data_bytes(&self) -> &Vec<u8> {
94        &self.tx_data_bytes
95    }
96
97    pub fn to_bcs_bytes(&self) -> Vec<u8> {
98        bcs::to_bytes(&self).unwrap()
99    }
100
101    pub fn to_move_bcs_bytes(&self) -> Vec<u8> {
102        bcs::to_bytes(&MoveAuthContext::default()).unwrap()
103    }
104
105    /// Returns whether the type signature is &mut AuthContext, &AuthContext, or
106    /// none of the above.
107    pub fn kind(module: &CompiledModule, token: &SignatureToken) -> AuthContextKind {
108        use SignatureToken as S;
109
110        let (kind, token) = match token {
111            S::MutableReference(token) => (AuthContextKind::Mutable, token),
112            S::Reference(token) => (AuthContextKind::Immutable, token),
113            _ => return AuthContextKind::None,
114        };
115
116        let S::Datatype(idx) = &**token else {
117            return AuthContextKind::None;
118        };
119
120        let (module_addr, module_name, struct_name) = resolve_struct(module, *idx);
121
122        if is_auth_context(module_addr, module_name, struct_name) {
123            kind
124        } else {
125            AuthContextKind::None
126        }
127    }
128
129    pub fn type_() -> StructTag {
130        StructTag {
131            address: IOTA_FRAMEWORK_ADDRESS,
132            module: AUTH_CONTEXT_MODULE_NAME.to_owned(),
133            name: AUTH_CONTEXT_STRUCT_NAME.to_owned(),
134            type_params: vec![],
135        }
136    }
137
138    /// Replaces the contents of the `AuthContext` with new values. This is
139    /// intended for use within a Move test function, as the `AuthContext`
140    /// should be immutable during normal use.
141    pub fn replace(
142        &mut self,
143        auth_digest: MoveAuthenticatorDigest,
144        tx_inputs: Vec<MoveCallArg>,
145        tx_commands: Vec<MoveCommand>,
146        tx_data_bytes: Vec<u8>,
147    ) {
148        self.auth_digest = auth_digest;
149        self.tx_inputs = tx_inputs;
150        self.tx_commands = tx_commands;
151        self.tx_data_bytes = tx_data_bytes;
152    }
153}
154
155/// A Move-side `AuthContext` representation.
156/// It is supposed to be used with empty fields since the Move `AuthContext`
157/// struct is managed by the native functions.
158#[derive(Default, Serialize)]
159pub struct MoveAuthContext {
160    auth_digest: MoveAuthenticatorDigest,
161    tx_inputs: Vec<MoveCallArg>,
162    tx_commands: Vec<MoveCommand>,
163}
164
165#[derive(PartialEq, Eq, Clone, Copy)]
166pub enum AuthContextKind {
167    // Not AuthContext
168    None,
169    // &mut AuthContext
170    Mutable,
171    // &AuthContext
172    Immutable,
173}
174
175pub fn is_auth_context(
176    module_addr: &AccountAddress,
177    module_name: &IdentStr,
178    struct_name: &IdentStr,
179) -> bool {
180    module_addr == &IOTA_FRAMEWORK_ADDRESS
181        && module_name == AUTH_CONTEXT_MODULE_NAME
182        && struct_name == AUTH_CONTEXT_STRUCT_NAME
183}
184
185#[cfg(test)]
186mod tests {
187
188    use super::*;
189    use crate::{
190        base_types::{Identifier, ObjectID, TypeTag},
191        transaction::{Argument, CallArg, Command, ProgrammableTransaction},
192    };
193
194    #[test]
195    fn auth_context_new_from_components() {
196        let ptb = ProgrammableTransaction {
197            inputs: vec![CallArg::Pure(vec![0xab])],
198            commands: vec![Command::new_move_call(
199                ObjectID::from_prefixed_short_hex("0x0000000000000000000000000000000000000001")
200                    .unwrap(),
201                Identifier::new_unchecked("mod"),
202                Identifier::new_unchecked("fun"),
203                vec![TypeTag::U8],
204                vec![Argument::Gas],
205            )],
206        };
207
208        let ctx =
209            AuthContext::new_from_components(MoveAuthenticatorDigest::default(), &ptb, vec![]);
210
211        assert_eq!(ctx.tx_inputs().len(), 1);
212        assert_eq!(ctx.tx_commands().len(), 1);
213
214        assert!(matches!(ctx.tx_inputs()[0], MoveCallArg::Pure(_)));
215
216        let MoveCommand::MoveCall(call) = &ctx.tx_commands()[0] else {
217            panic!("expected MoveCall");
218        };
219        assert_eq!(call.type_arguments, vec![TypeTag::U8]);
220    }
221
222    #[test]
223    fn auth_context_to_bcs_bytes_is_deterministic() {
224        let ctx = AuthContext::new_for_testing();
225        assert_eq!(ctx.to_bcs_bytes(), ctx.to_bcs_bytes());
226    }
227
228    #[test]
229    fn auth_context_to_bcs_bytes_reflects_content() {
230        let mut ctx = AuthContext::new_for_testing();
231        let empty_bytes = ctx.to_bcs_bytes();
232
233        ctx.replace(
234            MoveAuthenticatorDigest::default(),
235            vec![MoveCallArg::Pure(vec![1])],
236            vec![],
237            vec![],
238        );
239        let non_empty_bytes = ctx.to_bcs_bytes();
240
241        assert_ne!(empty_bytes, non_empty_bytes);
242    }
243}