Skip to main content

accumulate_client/codec/
hashes.rs

1//! Hash utilities that exactly match TypeScript SDK implementation
2//!
3//! This module provides SHA-256 hashing utilities with byte-for-byte compatibility
4//! with the TypeScript SDK for deterministic transaction and data hashing.
5
6use super::{canonical_json, BinaryWriter, EncodingError};
7use serde_json::Value;
8use sha2::{Digest, Sha256};
9
10/// Hash types used in Accumulate protocol
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum HashType {
13    /// SHA-256 hash of raw bytes
14    Sha256,
15    /// SHA-256 hash of canonical JSON
16    Sha256Json,
17    /// SHA-256 hash of binary-encoded data
18    Sha256Binary,
19}
20
21/// Hash utilities that match TypeScript SDK exactly
22#[derive(Debug, Clone, Copy)]
23pub struct AccumulateHash;
24
25impl AccumulateHash {
26    /// SHA-256 hash of raw bytes
27    /// Matches TS: hash(data: Uint8Array): Uint8Array
28    pub fn sha256_bytes(data: &[u8]) -> [u8; 32] {
29        let mut hasher = Sha256::new();
30        hasher.update(data);
31        hasher.finalize().into()
32    }
33
34    /// SHA-256 hash of raw bytes, returning hex string
35    /// Matches TS: hash(data: Uint8Array): string (when hex output requested)
36    pub fn sha256_bytes_hex(data: &[u8]) -> String {
37        let hash = Self::sha256_bytes(data);
38        hex::encode(hash)
39    }
40
41    /// SHA-256 hash of canonical JSON
42    /// Matches TS: hashJson(obj: any): Uint8Array
43    pub fn sha256_json(value: &Value) -> [u8; 32] {
44        let canonical = canonical_json(value);
45        Self::sha256_bytes(canonical.as_bytes())
46    }
47
48    /// SHA-256 hash of canonical JSON, returning hex string
49    /// Matches TS: hashJson(obj: any): string (when hex output requested)
50    pub fn sha256_json_hex(value: &Value) -> String {
51        let hash = Self::sha256_json(value);
52        hex::encode(hash)
53    }
54
55    /// Hash a string as UTF-8 bytes
56    /// Matches TS: hashString(str: string): Uint8Array
57    pub fn sha256_string(text: &str) -> [u8; 32] {
58        Self::sha256_bytes(text.as_bytes())
59    }
60
61    /// Hash a string as UTF-8 bytes, returning hex string
62    /// Matches TS: hashString(str: string): string (when hex output requested)
63    pub fn sha256_string_hex(text: &str) -> String {
64        let hash = Self::sha256_string(text);
65        hex::encode(hash)
66    }
67
68    /// Hash multiple byte arrays concatenated
69    /// Matches TS: hashConcat(...arrays: Uint8Array[]): Uint8Array
70    pub fn sha256_concat(arrays: &[&[u8]]) -> [u8; 32] {
71        let mut hasher = Sha256::new();
72        for array in arrays {
73            hasher.update(array);
74        }
75        hasher.finalize().into()
76    }
77
78    /// Hash multiple byte arrays concatenated, returning hex string
79    /// Matches TS: hashConcat(...arrays: Uint8Array[]): string (when hex output requested)
80    pub fn sha256_concat_hex(arrays: &[&[u8]]) -> String {
81        let hash = Self::sha256_concat(arrays);
82        hex::encode(hash)
83    }
84
85    /// Double SHA-256 hash (hash of hash)
86    /// Matches TS: doubleHash(data: Uint8Array): Uint8Array
87    pub fn double_sha256(data: &[u8]) -> [u8; 32] {
88        let first_hash = Self::sha256_bytes(data);
89        Self::sha256_bytes(&first_hash)
90    }
91
92    /// Double SHA-256 hash, returning hex string
93    /// Matches TS: doubleHash(data: Uint8Array): string (when hex output requested)
94    pub fn double_sha256_hex(data: &[u8]) -> String {
95        let hash = Self::double_sha256(data);
96        hex::encode(hash)
97    }
98
99    /// Hash using binary encoding first
100    /// Matches TS: hashBinaryEncoded(value: any, field?: number): Uint8Array
101    pub fn sha256_binary_encoded<T>(value: T, field: Option<u32>) -> Result<[u8; 32], EncodingError>
102    where
103        T: BinaryEncodable,
104    {
105        let binary_data = value.encode_binary()?;
106        let data = if let Some(field_num) = field {
107            BinaryWriter::with_field_number(&binary_data, Some(field_num))?
108        } else {
109            binary_data
110        };
111        Ok(Self::sha256_bytes(&data))
112    }
113
114    /// Hash using binary encoding first, returning hex string
115    /// Matches TS: hashBinaryEncoded(value: any, field?: number): string (when hex output requested)
116    pub fn sha256_binary_encoded_hex<T>(
117        value: T,
118        field: Option<u32>,
119    ) -> Result<String, EncodingError>
120    where
121        T: BinaryEncodable,
122    {
123        let hash = Self::sha256_binary_encoded(value, field)?;
124        Ok(hex::encode(hash))
125    }
126
127    /// Hash a transaction using canonical JSON (matches TS SDK exactly)
128    /// This is the primary method for transaction hashing in Accumulate
129    pub fn hash_transaction(transaction: &Value) -> [u8; 32] {
130        Self::sha256_json(transaction)
131    }
132
133    /// Hash a transaction using canonical JSON, returning hex string
134    pub fn hash_transaction_hex(transaction: &Value) -> String {
135        let hash = Self::hash_transaction(transaction);
136        hex::encode(hash)
137    }
138}
139
140/// Trait for types that can be binary encoded for hashing
141pub trait BinaryEncodable {
142    fn encode_binary(&self) -> Result<Vec<u8>, EncodingError>;
143}
144
145/// Implementation for basic types
146impl BinaryEncodable for u64 {
147    fn encode_binary(&self) -> Result<Vec<u8>, EncodingError> {
148        Ok(BinaryWriter::encode_uvarint(*self))
149    }
150}
151
152impl BinaryEncodable for i64 {
153    fn encode_binary(&self) -> Result<Vec<u8>, EncodingError> {
154        Ok(BinaryWriter::encode_varint(*self))
155    }
156}
157
158impl BinaryEncodable for String {
159    fn encode_binary(&self) -> Result<Vec<u8>, EncodingError> {
160        Ok(BinaryWriter::encode_string(self))
161    }
162}
163
164impl BinaryEncodable for &str {
165    fn encode_binary(&self) -> Result<Vec<u8>, EncodingError> {
166        Ok(BinaryWriter::encode_string(self))
167    }
168}
169
170impl BinaryEncodable for Vec<u8> {
171    fn encode_binary(&self) -> Result<Vec<u8>, EncodingError> {
172        Ok(BinaryWriter::encode_bytes(self))
173    }
174}
175
176impl BinaryEncodable for &[u8] {
177    fn encode_binary(&self) -> Result<Vec<u8>, EncodingError> {
178        Ok(BinaryWriter::encode_bytes(self))
179    }
180}
181
182impl BinaryEncodable for bool {
183    fn encode_binary(&self) -> Result<Vec<u8>, EncodingError> {
184        Ok(BinaryWriter::encode_bool(*self))
185    }
186}
187
188impl BinaryEncodable for [u8; 32] {
189    fn encode_binary(&self) -> Result<Vec<u8>, EncodingError> {
190        Ok(BinaryWriter::encode_hash(self))
191    }
192}
193
194/// URL hashing utilities that match TypeScript SDK
195#[derive(Debug, Clone, Copy)]
196pub struct UrlHash;
197
198impl UrlHash {
199    /// Hash an Accumulate URL for identity derivation
200    /// Matches TS: hashUrl(url: string): Uint8Array
201    pub fn hash_url(url: &str) -> [u8; 32] {
202        // TypeScript SDK uses specific URL normalization before hashing
203        let normalized = Self::normalize_url(url);
204        AccumulateHash::sha256_string(&normalized)
205    }
206
207    /// Hash an Accumulate URL, returning hex string
208    /// Matches TS: hashUrl(url: string): string (when hex output requested)
209    pub fn hash_url_hex(url: &str) -> String {
210        let hash = Self::hash_url(url);
211        hex::encode(hash)
212    }
213
214    /// Normalize Accumulate URL for consistent hashing
215    /// Matches TS URL normalization rules
216    pub fn normalize_url(url: &str) -> String {
217        let mut normalized = url.to_lowercase();
218
219        // Remove trailing slashes
220        while normalized.ends_with('/') {
221            normalized.pop();
222        }
223
224        // Ensure acc:// prefix
225        if !normalized.starts_with("acc://") {
226            if normalized.starts_with("//") {
227                normalized = format!("acc:{}", normalized);
228            } else if normalized.starts_with("/") {
229                normalized = format!("acc:/{}", normalized);
230            } else {
231                normalized = format!("acc://{}", normalized);
232            }
233        }
234
235        normalized
236    }
237
238    /// Derive key book URL from identity URL
239    /// Matches TS: deriveKeyBookUrl(identityUrl: string): string
240    pub fn derive_key_book_url(identity_url: &str) -> String {
241        let normalized = Self::normalize_url(identity_url);
242        format!("{}/book", normalized)
243    }
244
245    /// Derive key page URL from key book URL and page index
246    /// Matches TS: deriveKeyPageUrl(keyBookUrl: string, pageIndex: number): string
247    pub fn derive_key_page_url(key_book_url: &str, page_index: u32) -> String {
248        let normalized = Self::normalize_url(key_book_url);
249        format!("{}/{}", normalized, page_index)
250    }
251
252    /// Extract authority from Accumulate URL
253    /// Matches TS: extractAuthority(url: string): string
254    pub fn extract_authority(url: &str) -> Option<String> {
255        let normalized = Self::normalize_url(url);
256
257        if let Some(authority_start) = normalized.find("://") {
258            let after_protocol = &normalized[authority_start + 3..];
259            if let Some(path_start) = after_protocol.find('/') {
260                Some(after_protocol[..path_start].to_string())
261            } else {
262                Some(after_protocol.to_string())
263            }
264        } else {
265            None
266        }
267    }
268
269    /// Extract path from Accumulate URL
270    /// Matches TS: extractPath(url: string): string
271    pub fn extract_path(url: &str) -> String {
272        let normalized = Self::normalize_url(url);
273
274        if let Some(authority_start) = normalized.find("://") {
275            let after_protocol = &normalized[authority_start + 3..];
276            if let Some(path_start) = after_protocol.find('/') {
277                after_protocol[path_start..].to_string()
278            } else {
279                "/".to_string()
280            }
281        } else {
282            "/".to_string()
283        }
284    }
285}
286
287/// Chain ID hashing utilities
288#[derive(Debug, Clone, Copy)]
289pub struct ChainHash;
290
291impl ChainHash {
292    /// Hash chain ID from URL
293    /// Matches TS: hashChainId(url: string): Uint8Array
294    pub fn hash_chain_id(url: &str) -> [u8; 32] {
295        let normalized = UrlHash::normalize_url(url);
296        AccumulateHash::sha256_string(&normalized)
297    }
298
299    /// Hash chain ID from URL, returning hex string
300    /// Matches TS: hashChainId(url: string): string (when hex output requested)
301    pub fn hash_chain_id_hex(url: &str) -> String {
302        let hash = Self::hash_chain_id(url);
303        hex::encode(hash)
304    }
305
306    /// Derive main chain ID from authority
307    /// Matches TS: deriveMainChainId(authority: string): Uint8Array
308    pub fn derive_main_chain_id(authority: &str) -> [u8; 32] {
309        let main_url = format!("acc://{}", authority.to_lowercase());
310        Self::hash_chain_id(&main_url)
311    }
312
313    /// Derive main chain ID from authority, returning hex string
314    /// Matches TS: deriveMainChainId(authority: string): string (when hex output requested)
315    pub fn derive_main_chain_id_hex(authority: &str) -> String {
316        let hash = Self::derive_main_chain_id(authority);
317        hex::encode(hash)
318    }
319}
320
321/// Merkle tree utilities for transaction hashing
322#[derive(Debug, Clone, Copy)]
323pub struct MerkleHash;
324
325impl MerkleHash {
326    /// Build Merkle root from list of hashes
327    /// Matches TS: buildMerkleRoot(hashes: Uint8Array[]): Uint8Array
328    pub fn build_merkle_root(hashes: &[[u8; 32]]) -> [u8; 32] {
329        if hashes.is_empty() {
330            return [0u8; 32];
331        }
332
333        if hashes.len() == 1 {
334            return hashes[0];
335        }
336
337        let mut current_level: Vec<[u8; 32]> = hashes.to_vec();
338
339        while current_level.len() > 1 {
340            let mut next_level = Vec::new();
341
342            for chunk in current_level.chunks(2) {
343                let combined_hash = if chunk.len() == 2 {
344                    // Hash of concatenated pair
345                    AccumulateHash::sha256_concat(&[&chunk[0], &chunk[1]])
346                } else {
347                    // Odd number, hash single element with itself
348                    AccumulateHash::sha256_concat(&[&chunk[0], &chunk[0]])
349                };
350                next_level.push(combined_hash);
351            }
352
353            current_level = next_level;
354        }
355
356        current_level[0]
357    }
358
359    /// Build Merkle root from list of hashes, returning hex string
360    /// Matches TS: buildMerkleRoot(hashes: Uint8Array[]): string (when hex output requested)
361    pub fn build_merkle_root_hex(hashes: &[[u8; 32]]) -> String {
362        let root = Self::build_merkle_root(hashes);
363        hex::encode(root)
364    }
365
366    /// Create Merkle proof for element at index
367    /// Matches TS: createMerkleProof(hashes: Uint8Array[], index: number): Uint8Array[]
368    pub fn create_merkle_proof(hashes: &[[u8; 32]], index: usize) -> Vec<[u8; 32]> {
369        if hashes.is_empty() || index >= hashes.len() {
370            return Vec::new();
371        }
372
373        if hashes.len() == 1 {
374            return Vec::new();
375        }
376
377        let mut proof = Vec::new();
378        let mut current_level: Vec<[u8; 32]> = hashes.to_vec();
379        let mut current_index = index;
380
381        while current_level.len() > 1 {
382            // Find sibling hash
383            let sibling_index = if current_index % 2 == 0 {
384                current_index + 1
385            } else {
386                current_index - 1
387            };
388
389            if sibling_index < current_level.len() {
390                proof.push(current_level[sibling_index]);
391            } else {
392                // Odd number, sibling is the same element
393                proof.push(current_level[current_index]);
394            }
395
396            // Build next level
397            let mut next_level = Vec::new();
398            for chunk in current_level.chunks(2) {
399                let combined_hash = if chunk.len() == 2 {
400                    AccumulateHash::sha256_concat(&[&chunk[0], &chunk[1]])
401                } else {
402                    AccumulateHash::sha256_concat(&[&chunk[0], &chunk[0]])
403                };
404                next_level.push(combined_hash);
405            }
406
407            current_level = next_level;
408            current_index /= 2;
409        }
410
411        proof
412    }
413
414    /// Verify Merkle proof
415    /// Matches TS: verifyMerkleProof(root: Uint8Array, leaf: Uint8Array, proof: Uint8Array[], index: number): boolean
416    pub fn verify_merkle_proof(
417        root: &[u8; 32],
418        leaf: &[u8; 32],
419        proof: &[[u8; 32]],
420        index: usize,
421    ) -> bool {
422        let mut computed_hash = *leaf;
423        let mut current_index = index;
424
425        for sibling in proof {
426            computed_hash = if current_index % 2 == 0 {
427                AccumulateHash::sha256_concat(&[&computed_hash, sibling])
428            } else {
429                AccumulateHash::sha256_concat(&[sibling, &computed_hash])
430            };
431            current_index /= 2;
432        }
433
434        &computed_hash == root
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use serde_json::json;
442
443    #[test]
444    fn test_sha256_consistency() {
445        let data = b"hello world";
446        let hash1 = AccumulateHash::sha256_bytes(data);
447        let hash2 = AccumulateHash::sha256_bytes(data);
448        assert_eq!(hash1, hash2);
449
450        let hex1 = AccumulateHash::sha256_bytes_hex(data);
451        let hex2 = AccumulateHash::sha256_bytes_hex(data);
452        assert_eq!(hex1, hex2);
453        assert_eq!(hex1, hex::encode(hash1));
454    }
455
456    #[test]
457    fn test_json_hashing() {
458        let value = json!({
459            "name": "test",
460            "value": 42,
461            "array": [1, 2, 3]
462        });
463
464        let hash1 = AccumulateHash::sha256_json(&value);
465        let hash2 = AccumulateHash::sha256_json(&value);
466        assert_eq!(hash1, hash2);
467
468        // Test with reordered object - should produce same hash due to canonical JSON
469        let value2 = json!({
470            "value": 42,
471            "array": [1, 2, 3],
472            "name": "test"
473        });
474
475        let hash3 = AccumulateHash::sha256_json(&value2);
476        assert_eq!(hash1, hash3);
477    }
478
479    #[test]
480    fn test_url_normalization() {
481        let test_cases = vec![
482            ("acc://alice.acme", "acc://alice.acme"),
483            ("ACC://ALICE.ACME", "acc://alice.acme"),
484            ("acc://alice.acme/", "acc://alice.acme"),
485            ("acc://alice.acme///", "acc://alice.acme"),
486            ("//alice.acme", "acc://alice.acme"),
487            ("alice.acme", "acc://alice.acme"),
488            ("/alice.acme", "acc://alice.acme"),
489        ];
490
491        for (input, expected) in test_cases {
492            let normalized = UrlHash::normalize_url(input);
493            assert_eq!(normalized, expected, "Failed for input: {}", input);
494        }
495    }
496
497    #[test]
498    fn test_url_hashing() {
499        let url = "acc://alice.acme";
500        let hash1 = UrlHash::hash_url(url);
501        let hash2 = UrlHash::hash_url("ACC://ALICE.ACME/");
502        assert_eq!(
503            hash1, hash2,
504            "URL hashing should be case and trailing slash insensitive"
505        );
506
507        let hex = UrlHash::hash_url_hex(url);
508        assert_eq!(hex, hex::encode(hash1));
509    }
510
511    #[test]
512    fn test_url_derivation() {
513        let identity_url = "acc://alice.acme";
514        let key_book_url = UrlHash::derive_key_book_url(identity_url);
515        assert_eq!(key_book_url, "acc://alice.acme/book");
516
517        let key_page_url = UrlHash::derive_key_page_url(&key_book_url, 0);
518        assert_eq!(key_page_url, "acc://alice.acme/book/0");
519    }
520
521    #[test]
522    fn test_url_parsing() {
523        let url = "acc://alice.acme/tokens";
524        let authority = UrlHash::extract_authority(url).unwrap();
525        assert_eq!(authority, "alice.acme");
526
527        let path = UrlHash::extract_path(url);
528        assert_eq!(path, "/tokens");
529
530        let root_url = "acc://alice.acme";
531        let root_path = UrlHash::extract_path(root_url);
532        assert_eq!(root_path, "/");
533    }
534
535    #[test]
536    fn test_chain_id_hashing() {
537        let url = "acc://alice.acme";
538        let chain_id1 = ChainHash::hash_chain_id(url);
539        let chain_id2 = ChainHash::hash_chain_id("ACC://ALICE.ACME/");
540        assert_eq!(chain_id1, chain_id2);
541
542        let main_chain_id = ChainHash::derive_main_chain_id("alice.acme");
543        assert_eq!(chain_id1, main_chain_id);
544    }
545
546    #[test]
547    fn test_merkle_tree() {
548        let hashes = vec![[1u8; 32], [2u8; 32], [3u8; 32], [4u8; 32]];
549
550        let root = MerkleHash::build_merkle_root(&hashes);
551        assert_ne!(root, [0u8; 32]);
552
553        // Test single element
554        let single_root = MerkleHash::build_merkle_root(&hashes[0..1]);
555        assert_eq!(single_root, hashes[0]);
556
557        // Test empty
558        let empty_root = MerkleHash::build_merkle_root(&[]);
559        assert_eq!(empty_root, [0u8; 32]);
560    }
561
562    #[test]
563    fn test_merkle_proof() {
564        let hashes = vec![[1u8; 32], [2u8; 32], [3u8; 32], [4u8; 32]];
565
566        let root = MerkleHash::build_merkle_root(&hashes);
567        let proof = MerkleHash::create_merkle_proof(&hashes, 0);
568
569        let is_valid = MerkleHash::verify_merkle_proof(&root, &hashes[0], &proof, 0);
570        assert!(is_valid);
571
572        // Test invalid proof
573        let invalid_proof = MerkleHash::create_merkle_proof(&hashes, 1);
574        let is_invalid = MerkleHash::verify_merkle_proof(&root, &hashes[0], &invalid_proof, 0);
575        assert!(!is_invalid);
576    }
577
578    #[test]
579    fn test_double_hash() {
580        let data = b"test data";
581        let single_hash = AccumulateHash::sha256_bytes(data);
582        let double_hash = AccumulateHash::double_sha256(data);
583
584        assert_ne!(single_hash, double_hash);
585        assert_eq!(double_hash, AccumulateHash::sha256_bytes(&single_hash));
586    }
587
588    #[test]
589    fn test_concat_hash() {
590        let arrays = [b"hello".as_slice(), b" ".as_slice(), b"world".as_slice()];
591        let concat_hash = AccumulateHash::sha256_concat(&arrays);
592        let direct_hash = AccumulateHash::sha256_bytes(b"hello world");
593        assert_eq!(concat_hash, direct_hash);
594    }
595
596    #[test]
597    fn test_binary_encodable() {
598        let test_u64 = 12345u64;
599        let encoded = test_u64.encode_binary().unwrap();
600        assert_eq!(encoded, BinaryWriter::encode_uvarint(test_u64));
601
602        let test_string = "hello";
603        let encoded = test_string.encode_binary().unwrap();
604        assert_eq!(encoded, BinaryWriter::encode_string(test_string));
605
606        let test_bytes = vec![1, 2, 3, 4];
607        let encoded = test_bytes.encode_binary().unwrap();
608        assert_eq!(encoded, BinaryWriter::encode_bytes(&test_bytes));
609    }
610}