Skip to main content

iota_sdk_types/crypto/
intent.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5#[cfg(feature = "serde")]
6use std::str::FromStr;
7
8use crate::TreeDisplay;
9
10pub const INTENT_PREFIX_LENGTH: usize = 3;
11
12#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
14pub enum IntentError {
15    #[error("invalid bytes for Intent")]
16    Bytes,
17    #[error("invalid hex string for Intent")]
18    Hex,
19    #[error("invalid Scope for Intent")]
20    Scope,
21    #[error("invalid Version for Intent")]
22    Version,
23    #[error("invalid AppId for Intent")]
24    AppId,
25}
26
27/// A Signing Intent
28///
29/// An intent is a compact struct that serves as the domain separator for a
30/// message that a signature commits to. It consists of three parts:
31///     1. [enum IntentScope] (what the type of the message is)
32///     2. [enum IntentVersion]
33///     3. [enum IntentAppId] (what application the signature refers to).
34///
35/// The serialization of an Intent is a 3-byte array where each field is
36/// represented by a byte and it is prepended onto a message before it is signed
37/// in IOTA.
38///
39/// # BCS
40///
41/// The BCS serialized form for this type is defined by the following ABNF:
42///
43/// ```text
44/// intent = intent-scope intent-version intent-app-id
45/// ```
46#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
47#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
48#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
49pub struct Intent {
50    pub scope: IntentScope,
51    pub version: IntentVersion,
52    pub app_id: IntentAppId,
53}
54
55impl Intent {
56    pub fn new(scope: IntentScope, version: IntentVersion, app_id: IntentAppId) -> Self {
57        Self {
58            scope,
59            version,
60            app_id,
61        }
62    }
63
64    pub fn scope(self) -> IntentScope {
65        self.scope
66    }
67
68    pub fn version(self) -> IntentVersion {
69        self.version
70    }
71
72    pub fn app_id(self) -> IntentAppId {
73        self.app_id
74    }
75
76    pub fn iota_app(scope: IntentScope) -> Self {
77        Self {
78            scope,
79            version: IntentVersion::V0,
80            app_id: IntentAppId::Iota,
81        }
82    }
83
84    pub const fn iota_transaction() -> Self {
85        Self {
86            scope: IntentScope::TransactionData,
87            version: IntentVersion::V0,
88            app_id: IntentAppId::Iota,
89        }
90    }
91
92    pub const fn personal_message() -> Self {
93        Self {
94            scope: IntentScope::PersonalMessage,
95            version: IntentVersion::V0,
96            app_id: IntentAppId::Iota,
97        }
98    }
99
100    pub const fn consensus_app(scope: IntentScope) -> Self {
101        Self {
102            scope,
103            version: IntentVersion::V0,
104            app_id: IntentAppId::Consensus,
105        }
106    }
107
108    pub fn to_bytes(self) -> [u8; INTENT_PREFIX_LENGTH] {
109        [self.scope as u8, self.version as u8, self.app_id as u8]
110    }
111
112    #[cfg(feature = "serde")]
113    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, IntentError> {
114        let bytes = bytes.as_ref();
115        if bytes.len() != INTENT_PREFIX_LENGTH {
116            return Err(IntentError::Bytes);
117        }
118        Ok(Self {
119            scope: bytes[0].try_into()?,
120            version: bytes[1].try_into()?,
121            app_id: bytes[2].try_into()?,
122        })
123    }
124}
125
126impl crate::TreeDisplay for Intent {
127    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
128        w.header("Intent")?;
129        w.leaf("Scope", &self.scope, false)?;
130        w.leaf("Version", &self.version, false)?;
131        w.leaf("App ID", &self.app_id, true)
132    }
133}
134
135crate::impl_tree_display!(Intent);
136
137#[cfg(feature = "serde")]
138impl FromStr for Intent {
139    type Err = IntentError;
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        let bytes: Vec<u8> =
143            hex::decode(s.strip_prefix("0x").unwrap_or(s)).map_err(|_| IntentError::Hex)?;
144        Self::from_bytes(bytes.as_slice())
145    }
146}
147
148/// Byte signifying the scope of an [`Intent`]
149///
150/// This enum specifies the intent scope. Two intents for different scopes
151/// should never collide, so no signature provided for one intent scope can be
152/// used for another, even when the serialized data itself may be the same.
153///
154/// # BCS
155///
156/// The BCS serialized form for this type is defined by the following ABNF:
157///
158/// ```text
159/// intent-scope = u8
160/// ```
161#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, strum::Display)]
162#[cfg_attr(
163    feature = "serde",
164    derive(serde_repr::Deserialize_repr, serde_repr::Serialize_repr)
165)]
166#[repr(u8)]
167#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
168#[non_exhaustive]
169pub enum IntentScope {
170    TransactionData = 0,         // Used for a user signature on a transaction data.
171    TransactionEffects = 1,      // Used for an authority signature on transaction effects.
172    CheckpointSummary = 2,       // Used for an authority signature on a checkpoint summary.
173    PersonalMessage = 3,         // Used for a user signature on a personal message.
174    SenderSignedTransaction = 4, // Used for an authority signature on a user signed transaction.
175    ProofOfPossession = 5,       /* Used as a signature representing an authority's proof of
176                                  * possession of its authority key. */
177    BridgeEventDeprecated = 6, /* Deprecated. Should not be reused. Introduced for bridge
178                                * purposes but was never included in messages. */
179    ConsensusBlock = 7, // Used for consensus authority signature on block's digest.
180    DiscoveryPeers = 8, // Used for reporting peer addresses in discovery
181    AuthorityCapabilities = 9, // Used for authority capabilities from non-committee authorities.
182}
183
184impl IntentScope {
185    crate::def_is!(
186        TransactionData,
187        TransactionEffects,
188        CheckpointSummary,
189        PersonalMessage,
190        SenderSignedTransaction,
191        ProofOfPossession,
192        BridgeEventDeprecated,
193        ConsensusBlock,
194        DiscoveryPeers,
195        AuthorityCapabilities,
196    );
197}
198
199#[cfg(feature = "serde")]
200impl TryFrom<u8> for IntentScope {
201    type Error = IntentError;
202
203    fn try_from(value: u8) -> Result<Self, Self::Error> {
204        bcs::from_bytes(&[value]).map_err(|_| IntentError::Scope)
205    }
206}
207
208/// Byte signifying the version of an [`Intent`]
209///
210/// The version here is to distinguish between signing different versions of the
211/// struct or enum. Serialized output between two different versions of the same
212/// struct/enum might accidentally (or maliciously on purpose) match.
213///
214/// # BCS
215///
216/// The BCS serialized form for this type is defined by the following ABNF:
217///
218/// ```text
219/// intent-version = u8
220/// ```
221#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, strum::Display)]
222#[cfg_attr(
223    feature = "serde",
224    derive(serde_repr::Deserialize_repr, serde_repr::Serialize_repr)
225)]
226#[repr(u8)]
227#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
228#[non_exhaustive]
229pub enum IntentVersion {
230    V0 = 0,
231}
232
233impl IntentVersion {
234    crate::def_is!(V0);
235}
236
237#[cfg(feature = "serde")]
238impl TryFrom<u8> for IntentVersion {
239    type Error = IntentError;
240
241    fn try_from(value: u8) -> Result<Self, Self::Error> {
242        bcs::from_bytes(&[value]).map_err(|_| IntentError::Version)
243    }
244}
245
246/// Byte signifying the application id of an [`Intent`]
247///
248/// This enum specifies the application ID. Two intents in two different
249/// applications (i.e., IOTA, Ethereum etc) should never collide, so
250/// that even when a signing key is reused, nobody can take a signature
251/// designated for app_1 and present it as a valid signature for an (any) intent
252/// in app_2.
253///
254/// # BCS
255///
256/// The BCS serialized form for this type is defined by the following ABNF:
257///
258/// ```text
259/// intent-app-id = u8
260/// ```
261#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, strum::Display)]
262#[cfg_attr(
263    feature = "serde",
264    derive(serde_repr::Deserialize_repr, serde_repr::Serialize_repr)
265)]
266#[repr(u8)]
267#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
268#[non_exhaustive]
269pub enum IntentAppId {
270    Iota = 0,
271    Consensus = 1,
272}
273
274impl IntentAppId {
275    crate::def_is!(Iota, Consensus);
276}
277
278#[cfg(feature = "serde")]
279impl TryFrom<u8> for IntentAppId {
280    type Error = IntentError;
281
282    fn try_from(value: u8) -> Result<Self, Self::Error> {
283        bcs::from_bytes(&[value]).map_err(|_| IntentError::AppId)
284    }
285}
286
287/// Intent Message is a wrapper around a message with its intent. The message
288/// can be any type that implements [trait Serialize]. *ALL* signatures in IOTA
289/// must commit to the intent message, not the message itself. This guarantees
290/// any intent message signed in the system cannot collide with another since
291/// they are domain separated by intent.
292///
293/// The serialization of an IntentMessage is compact: it only prepends three
294/// bytes to the message itself.
295#[derive(Clone, Debug, Eq, Hash, PartialEq)]
296#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
297pub struct IntentMessage<T> {
298    pub intent: Intent,
299    pub value: T,
300}
301
302impl<T> IntentMessage<T> {
303    pub fn new(intent: Intent, value: T) -> Self {
304        Self { intent, value }
305    }
306}
307
308impl<T: std::fmt::Display> crate::TreeDisplay for IntentMessage<T> {
309    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
310        w.header("Intent Message")?;
311        w.child("Intent", &self.intent, false)?;
312        w.leaf("Value", &self.value, true)
313    }
314}
315
316impl<T: std::fmt::Display> std::fmt::Display for IntentMessage<T> {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        let mut w = crate::TreeWriter::new(f);
319        self.fmt_tree(&mut w)
320    }
321}
322
323/// A 1-byte domain separator for hashing Object ID in IOTA. It starts from
324/// 0xf0 to ensure no hashing collision for any ObjectID vs IotaAddress which is
325/// derived as the hash of `flag || pubkey`. See
326/// `iota_types::crypto::SignatureScheme::flag()`.
327#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, strum::Display)]
328#[cfg_attr(
329    feature = "serde",
330    derive(serde_repr::Deserialize_repr, serde_repr::Serialize_repr)
331)]
332#[repr(u8)]
333#[non_exhaustive]
334pub enum HashingIntentScope {
335    ChildObjectId = 0xf0,
336    RegularObjectId = 0xf1,
337}
338
339/// A personal message that wraps around a byte array.
340#[derive(Clone, Debug, Eq, PartialEq)]
341#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
342pub struct PersonalMessage<'a>(pub std::borrow::Cow<'a, [u8]>);
343
344impl std::fmt::Display for PersonalMessage<'_> {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        write!(f, "PersonalMessage({})", hex::encode(&self.0))
347    }
348}