Skip to main content

substrate/crypto/
hash.rs

1use anyhow::{anyhow, Result};
2use serde::Serialize;
3
4use super::AlgorithmBytes;
5
6const HASH_SEPARATOR: char = '.';
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum HashAlgorithm {
10    B3,
11}
12
13pub fn parse_hash(value: &str) -> Result<AlgorithmBytes<HashAlgorithm>> {
14    let (algorithm_str, value_b58) = value
15        .split_once(HASH_SEPARATOR)
16        .ok_or_else(|| anyhow!("Hash must use '{{algorithm}}.{{base58}}' format"))?;
17    let algorithm = match algorithm_str {
18        "b3" => HashAlgorithm::B3,
19        other => return Err(anyhow!("Unsupported hash algorithm: '{}'", other)),
20    };
21    if value_b58.is_empty() {
22        return Err(anyhow!("Hash payload must not be empty"));
23    }
24
25    let bytes = bs58::decode(value_b58)
26        .into_vec()
27        .map_err(|e| anyhow!("Invalid hash base58 payload: {}", e))?;
28    if bytes.is_empty() {
29        return Err(anyhow!("Hash payload must not decode to empty bytes"));
30    }
31
32    Ok(AlgorithmBytes { algorithm, bytes })
33}
34
35pub fn format_hash(algorithm: HashAlgorithm, bytes: &[u8]) -> String {
36    format!(
37        "{}.{}",
38        algorithm.prefix(),
39        bs58::encode(bytes).into_string()
40    )
41}
42
43/// Compute a raw BLAKE3 hash using CMN hash formatting.
44pub fn compute_blake3_hash(data: &[u8]) -> String {
45    let hash = blake3::hash(data);
46    format_hash(HashAlgorithm::B3, hash.as_bytes())
47}
48
49pub(crate) fn compute_signed_core_hash<T: Serialize>(
50    core: &T,
51    core_signature: &str,
52) -> Result<String> {
53    let hash_input = serde_json::json!({
54        "core": core,
55        "core_signature": core_signature,
56    });
57    let canonical = serde_jcs::to_string(&hash_input)
58        .map_err(|e| anyhow!("JCS serialization failed: {}", e))?;
59    Ok(compute_blake3_hash(canonical.as_bytes()))
60}
61
62pub(crate) fn compute_tree_signed_core_hash<T: Serialize>(
63    tree_hash: &str,
64    core: &T,
65    core_signature: &str,
66) -> Result<String> {
67    let hash_input = serde_json::json!({
68        "tree_hash": tree_hash,
69        "core": core,
70        "core_signature": core_signature,
71    });
72    let canonical = serde_jcs::to_string(&hash_input)
73        .map_err(|e| anyhow!("JCS serialization failed: {}", e))?;
74    Ok(compute_blake3_hash(canonical.as_bytes()))
75}
76
77impl HashAlgorithm {
78    fn prefix(self) -> &'static str {
79        match self {
80            Self::B3 => "b3",
81        }
82    }
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
87mod tests {
88
89    use super::*;
90
91    #[test]
92    fn test_parse_hash_roundtrip() {
93        let parsed = parse_hash("b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2").unwrap();
94        assert_eq!(parsed.algorithm, HashAlgorithm::B3);
95        let normalized = format_hash(parsed.algorithm, &parsed.bytes);
96        assert_eq!(normalized, "b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2");
97    }
98
99    #[test]
100    fn test_parse_hash_rejects_unknown_algorithm() {
101        assert!(parse_hash("sha256.abc").is_err());
102    }
103
104    #[test]
105    fn test_compute_blake3_hash() {
106        let hash = compute_blake3_hash(b"hello");
107        assert!(hash.starts_with("b3."));
108        assert!(hash.len() > 10);
109        assert_eq!(hash, compute_blake3_hash(b"hello"));
110        assert_ne!(hash, compute_blake3_hash(b"world"));
111    }
112
113    #[test]
114    fn test_blake3_hash_for_content_addressing() {
115        let content = r#"{"name":"test","version":"1.0"}"#;
116        let hash1 = compute_blake3_hash(content.as_bytes());
117        let hash2 = compute_blake3_hash(content.as_bytes());
118        assert_eq!(hash1, hash2);
119
120        let different_content = r#"{"name":"test","version":"1.1"}"#;
121        let hash3 = compute_blake3_hash(different_content.as_bytes());
122        assert_ne!(hash1, hash3);
123    }
124
125    #[test]
126    fn test_compute_signed_core_hash_is_deterministic() {
127        let core = serde_json::json!({"name": "cmn.dev", "updated_at_epoch_ms": 1});
128        let signature = "ed25519.test-signature";
129
130        let hash1 = compute_signed_core_hash(&core, signature).unwrap();
131        let hash2 = compute_signed_core_hash(&core, signature).unwrap();
132
133        assert_eq!(hash1, hash2);
134    }
135
136    #[test]
137    fn test_compute_tree_signed_core_hash_changes_with_tree_hash() {
138        let core = serde_json::json!({"name": "cmn-spec"});
139        let signature = "ed25519.test-signature";
140
141        let hash1 = compute_tree_signed_core_hash("b3.tree-a", &core, signature).unwrap();
142        let hash2 = compute_tree_signed_core_hash("b3.tree-b", &core, signature).unwrap();
143
144        assert_ne!(hash1, hash2);
145    }
146}