Skip to main content

quantus_cli/subsquid/
hash.rs

1//! Hash utilities for privacy-preserving queries.
2//!
3//! Uses blake3 to compute address hashes that match the Subsquid indexer.
4
5/// Compute blake3 hash of raw address bytes and return as hex string.
6///
7/// This matches the hash computation done by the Subsquid indexer.
8///
9/// # Arguments
10///
11/// * `raw_address` - The raw 32-byte account ID
12///
13/// # Returns
14///
15/// The blake3 hash as a 64-character hex string
16pub fn compute_address_hash(raw_address: &[u8; 32]) -> String {
17	let hash = blake3::hash(raw_address);
18	hex::encode(hash.as_bytes())
19}
20
21/// Get a prefix of the specified length from a hash.
22///
23/// # Arguments
24///
25/// * `hash` - The full hash as a hex string
26/// * `prefix_len` - The number of hex characters to include in the prefix
27///
28/// # Returns
29///
30/// The prefix as a hex string
31pub fn get_hash_prefix(hash: &str, prefix_len: usize) -> String {
32	hash.chars().take(prefix_len).collect()
33}
34
35#[cfg(test)]
36mod tests {
37	use super::*;
38
39	// Known test vectors - these values are verified against the TypeScript implementation
40	const ZERO_BYTES_HASH: &str =
41		"2ada83c1819a5372dae1238fc1ded123c8104fdaa15862aaee69428a1820fcda";
42	const ONES_BYTES_HASH: &str =
43		"9b34f060fbc0f0aa11f150e26519deff613277b60656f0f8356ed2261505f5c5";
44	const SEQUENTIAL_BYTES_HASH: &str =
45		"e528e95798037df410543d9f31e396ecdd458d71b157d6014398bae32fb56c65";
46
47	#[test]
48	fn test_known_hash_vectors() {
49		// These test vectors ensure Rust and TypeScript produce identical hashes
50		assert_eq!(compute_address_hash(&[0u8; 32]), ZERO_BYTES_HASH);
51		assert_eq!(compute_address_hash(&[0xffu8; 32]), ONES_BYTES_HASH);
52
53		let mut sequential = [0u8; 32];
54		for (i, byte) in sequential.iter_mut().enumerate() {
55			*byte = i as u8;
56		}
57		assert_eq!(compute_address_hash(&sequential), SEQUENTIAL_BYTES_HASH);
58	}
59
60	#[test]
61	fn test_hash_format() {
62		let hash = compute_address_hash(&[0u8; 32]);
63		assert_eq!(hash.len(), 64);
64		assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
65	}
66
67	#[test]
68	fn test_hash_determinism() {
69		let address = [42u8; 32];
70		assert_eq!(compute_address_hash(&address), compute_address_hash(&address));
71	}
72
73	#[test]
74	fn test_different_inputs_different_hashes() {
75		assert_ne!(compute_address_hash(&[1u8; 32]), compute_address_hash(&[2u8; 32]));
76	}
77
78	#[test]
79	fn test_get_hash_prefix() {
80		let hash = "abcdef1234567890";
81		assert_eq!(get_hash_prefix(hash, 0), "");
82		assert_eq!(get_hash_prefix(hash, 2), "ab");
83		assert_eq!(get_hash_prefix(hash, 4), "abcd");
84		assert_eq!(get_hash_prefix(hash, 100), hash); // longer than input returns full string
85	}
86}