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::ObjectID,
191        transaction::{Argument, CallArg, Command, ProgrammableMoveCall, ProgrammableTransaction},
192        type_input::{TypeInput, TypeName},
193    };
194
195    #[test]
196    fn auth_context_new_from_components() {
197        let ptb = ProgrammableTransaction {
198            inputs: vec![CallArg::Pure(vec![0xab])],
199            commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
200                package: ObjectID::from_hex_literal("0x0000000000000000000000000000000000000001")
201                    .unwrap(),
202                module: "mod".to_string(),
203                function: "fun".to_string(),
204                type_arguments: vec![TypeInput::U8],
205                arguments: vec![Argument::GasCoin],
206            }))],
207        };
208
209        let ctx =
210            AuthContext::new_from_components(MoveAuthenticatorDigest::default(), &ptb, vec![]);
211
212        assert_eq!(ctx.tx_inputs().len(), 1);
213        assert_eq!(ctx.tx_commands().len(), 1);
214
215        assert!(matches!(ctx.tx_inputs()[0], MoveCallArg::Pure(_)));
216
217        // Commands must have TypeName substituted for TypeInput.
218        let MoveCommand::MoveCall(call) = &ctx.tx_commands()[0] else {
219            panic!("expected MoveCall");
220        };
221        assert_eq!(
222            call.type_arguments,
223            vec![TypeName {
224                name: "u8".to_string()
225            }]
226        );
227    }
228
229    #[test]
230    fn auth_context_to_bcs_bytes_is_deterministic() {
231        let ctx = AuthContext::new_for_testing();
232        assert_eq!(ctx.to_bcs_bytes(), ctx.to_bcs_bytes());
233    }
234
235    #[test]
236    fn auth_context_to_bcs_bytes_reflects_content() {
237        let mut ctx = AuthContext::new_for_testing();
238        let empty_bytes = ctx.to_bcs_bytes();
239
240        ctx.replace(
241            MoveAuthenticatorDigest::default(),
242            vec![MoveCallArg::Pure(vec![1])],
243            vec![],
244            vec![],
245        );
246        let non_empty_bytes = ctx.to_bcs_bytes();
247
248        assert_ne!(empty_bytes, non_empty_bytes);
249    }
250}