iota_sdk_types/
digest.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5/// A 32-byte Blake2b256 hash output.
6///
7/// # BCS
8///
9/// A `Digest`'s BCS serialized form is defined by the following:
10///
11/// ```text
12/// digest = %x20 32OCTET
13/// ```
14///
15/// Due to historical reasons, even though a `Digest` has a fixed-length of 32,
16/// IOTA's binary representation of a `Digest` is prefixed with its length
17/// meaning its serialized binary form (in bcs) is 33 bytes long vs a more
18/// compact 32 bytes.
19#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
23pub struct Digest(
24    #[cfg_attr(feature = "serde", serde(with = "DigestSerialization"))]
25    #[cfg_attr(feature = "schemars", schemars(with = "crate::_schemars::Base58"))]
26    [u8; Self::LENGTH],
27);
28
29impl Digest {
30    /// A constant representing the length of a digest in bytes.
31    pub const LENGTH: usize = 32;
32    /// A constant representing a zero digest.
33    pub const ZERO: Self = Self([0; Self::LENGTH]);
34
35    /// Generates a new digest from the provided 32 byte array containing [`u8`]
36    /// values.
37    pub const fn new(digest: [u8; Self::LENGTH]) -> Self {
38        Self(digest)
39    }
40
41    /// Generates a new digest from the provided random number generator.
42    #[cfg(feature = "rand")]
43    #[cfg_attr(doc_cfg, doc(cfg(feature = "rand")))]
44    pub fn generate<R>(mut rng: R) -> Self
45    where
46        R: rand_core::RngCore + rand_core::CryptoRng,
47    {
48        let mut buf: [u8; Self::LENGTH] = [0; Self::LENGTH];
49        rng.fill_bytes(&mut buf);
50        Self::new(buf)
51    }
52
53    /// Returns a slice to the inner array representation of this digest.
54    pub const fn inner(&self) -> &[u8; Self::LENGTH] {
55        &self.0
56    }
57
58    /// Returns the inner array representation of this digest.
59    pub const fn into_inner(self) -> [u8; Self::LENGTH] {
60        self.0
61    }
62
63    /// Returns a slice of bytes representing the digest.
64    pub const fn as_bytes(&self) -> &[u8] {
65        &self.0
66    }
67
68    /// Decodes a digest from a Base58 encoded string.
69    pub fn from_base58<T: AsRef<[u8]>>(base58: T) -> Result<Self, DigestParseError> {
70        let mut buf = [0; Self::LENGTH];
71
72        bs58::decode(base58)
73            .onto(&mut buf)
74            // TODO fix error to contain bs58 parse error
75            .map_err(|_| DigestParseError)?;
76
77        Ok(Self(buf))
78    }
79
80    /// Returns a Base58 encoded string representation of this digest.
81    pub fn to_base58(&self) -> String {
82        self.to_string()
83    }
84
85    /// Generates a digest from bytes.
86    pub fn from_bytes<T: AsRef<[u8]>>(bytes: T) -> Result<Self, DigestParseError> {
87        <[u8; Self::LENGTH]>::try_from(bytes.as_ref())
88            .map_err(|_| DigestParseError)
89            .map(Self)
90    }
91}
92
93impl std::str::FromStr for Digest {
94    type Err = DigestParseError;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        Self::from_base58(s)
98    }
99}
100
101impl AsRef<[u8]> for Digest {
102    fn as_ref(&self) -> &[u8] {
103        &self.0
104    }
105}
106
107impl AsRef<[u8; Self::LENGTH]> for Digest {
108    fn as_ref(&self) -> &[u8; Self::LENGTH] {
109        &self.0
110    }
111}
112
113impl From<Digest> for [u8; Digest::LENGTH] {
114    fn from(digest: Digest) -> Self {
115        digest.into_inner()
116    }
117}
118
119impl From<[u8; Self::LENGTH]> for Digest {
120    fn from(digest: [u8; Self::LENGTH]) -> Self {
121        Self::new(digest)
122    }
123}
124
125impl std::fmt::Display for Digest {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        // output size is determined via the following formula:
128        //      N * log(256) / log(58) + 1 (round up)
129        // where N = 32 this results in a value of 45
130        let mut buf = [0; 45];
131
132        let len = bs58::encode(&self.0).onto(&mut buf[..]).unwrap();
133        let encoded = std::str::from_utf8(&buf[..len]).unwrap();
134
135        f.write_str(encoded)
136    }
137}
138
139impl std::fmt::Debug for Digest {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.debug_tuple("Digest")
142            .field(&format_args!("\"{self}\""))
143            .finish()
144    }
145}
146
147impl std::fmt::LowerHex for Digest {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        if f.alternate() {
150            write!(f, "0x")?;
151        }
152
153        for byte in self.0 {
154            write!(f, "{byte:02x}")?;
155        }
156
157        Ok(())
158    }
159}
160
161// Unfortunately IOTA's binary representation of digests is prefixed with its
162// length meaning its serialized binary form is 33 bytes long (in bcs) vs a more
163// compact 32 bytes.
164#[cfg(feature = "serde")]
165type DigestSerialization =
166    ::serde_with::As<::serde_with::IfIsHumanReadable<ReadableDigest, ::serde_with::Bytes>>;
167
168#[cfg(feature = "serde")]
169#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
170struct ReadableDigest;
171
172#[cfg(feature = "serde")]
173#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
174impl serde_with::SerializeAs<[u8; Digest::LENGTH]> for ReadableDigest {
175    fn serialize_as<S>(source: &[u8; Digest::LENGTH], serializer: S) -> Result<S::Ok, S::Error>
176    where
177        S: serde::Serializer,
178    {
179        let digest = Digest::new(*source);
180        serde_with::DisplayFromStr::serialize_as(&digest, serializer)
181    }
182}
183
184#[cfg(feature = "serde")]
185#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
186impl<'de> serde_with::DeserializeAs<'de, [u8; Digest::LENGTH]> for ReadableDigest {
187    fn deserialize_as<D>(deserializer: D) -> Result<[u8; Digest::LENGTH], D::Error>
188    where
189        D: serde::Deserializer<'de>,
190    {
191        let digest: Digest = serde_with::DisplayFromStr::deserialize_as(deserializer)?;
192        Ok(digest.into_inner())
193    }
194}
195
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub struct DigestParseError;
198
199impl std::fmt::Display for DigestParseError {
200    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
201        write!(
202            f,
203            "Unable to parse Digest (must be Base58 string of length {})",
204            44,
205        )
206    }
207}
208
209impl std::error::Error for DigestParseError {}
210
211// Don't implement like the other digest type since this isn't intended to be
212// serialized
213pub type SigningDigest = [u8; Digest::LENGTH];
214
215#[cfg(test)]
216mod tests {
217    use test_strategy::proptest;
218
219    use super::*;
220
221    #[proptest]
222    fn roundtrip_display_fromstr(digest: Digest) {
223        let s = digest.to_string();
224        let d = s.parse::<Digest>().unwrap();
225        assert_eq!(digest, d);
226    }
227}