From ad99e2f85880ab55ce32872ad8247187f4833eb6 Mon Sep 17 00:00:00 2001 From: Alexey Gradoboev Date: Wed, 17 Jun 2026 17:51:18 +0300 Subject: [PATCH 1/2] fix: make Hrp Hash case-insensitive to match PartialEq Fixes the Hash implementation for the Hrp struct to iterate over lowercase bytes instead of hashing the raw internal buffer. This restores the core invariant where `a == b` implies `hash(a) == hash(b)`. Fixes: #275 --- src/primitives/hrp.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/primitives/hrp.rs b/src/primitives/hrp.rs index b418fc1e..709df8a3 100644 --- a/src/primitives/hrp.rs +++ b/src/primitives/hrp.rs @@ -331,7 +331,14 @@ impl Eq for Hrp {} impl core::hash::Hash for Hrp { #[inline] - fn hash(&self, h: &mut H) { self.buf.hash(h) } + fn hash(&self, h: &mut H) { + h.write_usize(self.size); + let mut buf = [0u8; MAX_HRP_LEN]; + for (idx, byte) in self.lowercase_byte_iter().enumerate() { + buf[idx] = byte; + } + h.write(&buf[..self.size]); + } } /// Iterator over bytes (ASCII values) of the human-readable part. From 0232d7b091bc74216476b9ed7573ea5368c69b1d Mon Sep 17 00:00:00 2001 From: Alexey Gradoboev Date: Tue, 16 Jun 2026 22:42:59 +0300 Subject: [PATCH 2/2] test: adding test for Hrp case-insensitive Hash Fixes the Hash implementation for the Hrp struct to iterate over lowercase bytes instead of hashing the raw internal buffer. This restores the core invariant where `a == b` implies `hash(a) == hash(b)`. The commit only adds a new test to reproduce the issue, no real fix. Fixes: #275 --- src/primitives/hrp.rs | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/primitives/hrp.rs b/src/primitives/hrp.rs index 709df8a3..cf75edc6 100644 --- a/src/primitives/hrp.rs +++ b/src/primitives/hrp.rs @@ -779,4 +779,54 @@ mod tests { assert_eq!(hrp.as_bytes()[1], b'X'); assert_eq!(hrp.as_bytes()[2], b'~'); } + + struct Simple(u64); + + impl Hasher for Simple { + fn finish(&self) -> u64 { self.0 } + fn write(&mut self, bytes: &[u8]) { + for &b in bytes { + self.0 = self.0.wrapping_mul(31).wrapping_add(b as u64); + } + } + } + + #[test] + fn hash_consistent_with_eq() { + let get_hash = |hrp: &Hrp| { + let mut hasher = Simple(0); + hrp.hash(&mut hasher); + hasher.finish() + }; + + let a = Hrp::parse_unchecked("ABC"); + let b = Hrp::parse_unchecked("abc"); + assert_eq!(a, b); + assert_eq!(get_hash(&a), get_hash(&b)); + + let mixed_1 = Hrp::parse_unchecked("aBcDeFg"); + let mixed_2 = Hrp::parse_unchecked("AbCdEfG"); + assert_eq!(mixed_1, mixed_2); + assert_eq!(get_hash(&mixed_1), get_hash(&mixed_2)); + + let num_1 = &Hrp::parse_unchecked("bc1"); + let num_2 = &Hrp::parse_unchecked("BC1"); + assert_eq!(num_1, num_2); + assert_eq!(get_hash(num_1), get_hash(num_2)); + + let short = Hrp::parse_unchecked("abc"); + let long = Hrp::parse_unchecked("abcd"); + assert_ne!(short, long); + assert_ne!(get_hash(&short), get_hash(&long)); + + let get_composite_hash = |item: &(Hrp, &str)| { + let mut hasher = Simple(0); + item.hash(&mut hasher); + hasher.finish() + }; + let composite_1 = (short, "def"); + let composite_2 = (long, "ef"); + assert_ne!(composite_1, composite_2); + assert_ne!(get_composite_hash(&composite_1), get_composite_hash(&composite_2)); + } }