Skip to main content

near_kit/types/
hash.rs

1//! Cryptographic hash type.
2
3use std::fmt::{self, Debug, Display};
4use std::str::FromStr;
5
6use borsh::{BorshDeserialize, BorshSerialize};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use sha2::{Digest, Sha256};
9
10use crate::error::ParseHashError;
11
12/// A 32-byte SHA-256 hash used for block hashes, transaction hashes, etc.
13#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
14pub struct CryptoHash([u8; 32]);
15
16impl CryptoHash {
17    /// The zero hash (32 zero bytes).
18    pub const ZERO: Self = Self([0; 32]);
19
20    /// Hash the given data with SHA-256.
21    pub fn hash(data: &[u8]) -> Self {
22        let result = Sha256::digest(data);
23        let mut bytes = [0u8; 32];
24        bytes.copy_from_slice(&result);
25        Self(bytes)
26    }
27
28    /// Create from raw 32 bytes.
29    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
30        Self(bytes)
31    }
32
33    /// Get the raw 32 bytes.
34    pub const fn as_bytes(&self) -> &[u8; 32] {
35        &self.0
36    }
37
38    /// Convert to a `Vec<u8>`.
39    pub fn to_vec(&self) -> Vec<u8> {
40        self.0.to_vec()
41    }
42
43    /// Check if this is the zero hash.
44    pub fn is_zero(&self) -> bool {
45        self.0 == [0u8; 32]
46    }
47}
48
49impl FromStr for CryptoHash {
50    type Err = ParseHashError;
51
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        let bytes = bs58::decode(s)
54            .into_vec()
55            .map_err(|e| ParseHashError::InvalidBase58(e.to_string()))?;
56
57        if bytes.len() != 32 {
58            return Err(ParseHashError::InvalidLength(bytes.len()));
59        }
60
61        let mut arr = [0u8; 32];
62        arr.copy_from_slice(&bytes);
63        Ok(Self(arr))
64    }
65}
66
67impl TryFrom<&str> for CryptoHash {
68    type Error = ParseHashError;
69
70    fn try_from(s: &str) -> Result<Self, Self::Error> {
71        s.parse()
72    }
73}
74
75impl TryFrom<&[u8]> for CryptoHash {
76    type Error = ParseHashError;
77
78    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
79        if bytes.len() != 32 {
80            return Err(ParseHashError::InvalidLength(bytes.len()));
81        }
82        let mut arr = [0u8; 32];
83        arr.copy_from_slice(bytes);
84        Ok(Self(arr))
85    }
86}
87
88impl From<[u8; 32]> for CryptoHash {
89    fn from(bytes: [u8; 32]) -> Self {
90        Self(bytes)
91    }
92}
93
94impl AsRef<[u8]> for CryptoHash {
95    fn as_ref(&self) -> &[u8] {
96        &self.0
97    }
98}
99
100impl Display for CryptoHash {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "{}", bs58::encode(&self.0).into_string())
103    }
104}
105
106impl Debug for CryptoHash {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "CryptoHash({})", self)
109    }
110}
111
112impl Serialize for CryptoHash {
113    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
114        s.serialize_str(&self.to_string())
115    }
116}
117
118impl<'de> Deserialize<'de> for CryptoHash {
119    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
120        let s: String = serde::Deserialize::deserialize(d)?;
121        s.parse().map_err(serde::de::Error::custom)
122    }
123}
124
125impl BorshSerialize for CryptoHash {
126    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
127        writer.write_all(&self.0)
128    }
129}
130
131impl BorshDeserialize for CryptoHash {
132    fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
133        let mut bytes = [0u8; 32];
134        reader.read_exact(&mut bytes)?;
135        Ok(Self(bytes))
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_hash() {
145        let hash = CryptoHash::hash(b"hello world");
146        assert!(!hash.is_zero());
147        assert_eq!(hash.as_bytes().len(), 32);
148    }
149
150    #[test]
151    fn test_display_parse_roundtrip() {
152        let hash = CryptoHash::hash(b"test data");
153        let s = hash.to_string();
154        let parsed: CryptoHash = s.parse().unwrap();
155        assert_eq!(hash, parsed);
156    }
157
158    #[test]
159    fn test_zero() {
160        assert!(CryptoHash::ZERO.is_zero());
161        assert!(!CryptoHash::hash(b"x").is_zero());
162    }
163
164    #[test]
165    fn test_from_bytes() {
166        let bytes = [42u8; 32];
167        let hash = CryptoHash::from_bytes(bytes);
168        assert_eq!(hash.as_bytes(), &bytes);
169    }
170
171    #[test]
172    fn test_to_vec() {
173        let hash = CryptoHash::hash(b"test");
174        let vec = hash.to_vec();
175        assert_eq!(vec.len(), 32);
176        assert_eq!(vec.as_slice(), hash.as_bytes());
177    }
178
179    #[test]
180    fn test_from_32_byte_array() {
181        let bytes = [1u8; 32];
182        let hash: CryptoHash = bytes.into();
183        assert_eq!(hash.as_bytes(), &bytes);
184    }
185
186    #[test]
187    fn test_try_from_slice_success() {
188        let bytes = [2u8; 32];
189        let hash = CryptoHash::try_from(bytes.as_slice()).unwrap();
190        assert_eq!(hash.as_bytes(), &bytes);
191    }
192
193    #[test]
194    fn test_try_from_slice_wrong_length() {
195        let bytes = [3u8; 16]; // Wrong length
196        let result = CryptoHash::try_from(bytes.as_slice());
197        assert!(matches!(
198            result,
199            Err(crate::error::ParseHashError::InvalidLength(16))
200        ));
201    }
202
203    #[test]
204    fn test_try_from_str() {
205        let hash = CryptoHash::hash(b"test");
206        let s = hash.to_string();
207        let parsed = CryptoHash::try_from(s.as_str()).unwrap();
208        assert_eq!(hash, parsed);
209    }
210
211    #[test]
212    fn test_as_ref() {
213        let hash = CryptoHash::hash(b"test");
214        let slice: &[u8] = hash.as_ref();
215        assert_eq!(slice.len(), 32);
216        assert_eq!(slice, hash.as_bytes());
217    }
218
219    #[test]
220    fn test_debug_format() {
221        let hash = CryptoHash::ZERO;
222        let debug = format!("{:?}", hash);
223        assert!(debug.starts_with("CryptoHash("));
224        assert!(debug.contains("1111111111")); // Zero hash in base58
225    }
226
227    #[test]
228    fn test_parse_invalid_base58() {
229        // Invalid base58 characters
230        let result: Result<CryptoHash, _> = "invalid!@#$%base58".parse();
231        assert!(matches!(
232            result,
233            Err(crate::error::ParseHashError::InvalidBase58(_))
234        ));
235    }
236
237    #[test]
238    fn test_parse_wrong_length() {
239        // Valid base58 but wrong length (too short)
240        let result: Result<CryptoHash, _> = "3xRDxw".parse();
241        assert!(matches!(
242            result,
243            Err(crate::error::ParseHashError::InvalidLength(_))
244        ));
245    }
246
247    #[test]
248    fn test_serde_roundtrip() {
249        let hash = CryptoHash::hash(b"serde test");
250        let json = serde_json::to_string(&hash).unwrap();
251        let parsed: CryptoHash = serde_json::from_str(&json).unwrap();
252        assert_eq!(hash, parsed);
253    }
254
255    #[test]
256    fn test_borsh_roundtrip() {
257        let hash = CryptoHash::hash(b"borsh test");
258        let bytes = borsh::to_vec(&hash).unwrap();
259        assert_eq!(bytes.len(), 32);
260        let parsed: CryptoHash = borsh::from_slice(&bytes).unwrap();
261        assert_eq!(hash, parsed);
262    }
263
264    #[test]
265    fn test_hash_deterministic() {
266        let hash1 = CryptoHash::hash(b"same input");
267        let hash2 = CryptoHash::hash(b"same input");
268        assert_eq!(hash1, hash2);
269
270        let hash3 = CryptoHash::hash(b"different input");
271        assert_ne!(hash1, hash3);
272    }
273
274    #[test]
275    fn test_default() {
276        let hash = CryptoHash::default();
277        assert!(hash.is_zero());
278        assert_eq!(hash, CryptoHash::ZERO);
279    }
280
281    #[test]
282    fn test_clone() {
283        let hash1 = CryptoHash::hash(b"clone test");
284        #[allow(clippy::clone_on_copy)]
285        let hash2 = hash1.clone(); // Intentionally testing Clone impl
286        assert_eq!(hash1, hash2);
287    }
288
289    #[test]
290    fn test_hash_comparison() {
291        let hash1 = CryptoHash::from_bytes([0u8; 32]);
292        let hash2 = CryptoHash::from_bytes([1u8; 32]);
293        // CryptoHash doesn't implement Ord, but we can compare for equality
294        assert_ne!(hash1, hash2);
295        assert_eq!(hash1, CryptoHash::ZERO);
296    }
297}