为什么Python和Rust的同一个哈希函数对同一个字符串会产生不同的结果?

yws3nbqq  于 2023-02-16  发布在  Python
关注(0)|答案(1)|浏览(170)

靶病变; DR:
使用相同的参数,两个散列函数产生相同的结果。要实现这一点,需要满足的前提条件很少。
我正在构建一个包含Rust和Python部分的系统。我需要一个哈希库,在两端为相同的输入生成相同的值。我认为Python和Rust也使用SipHash 1 - 3,所以我尝试使用它。
巨蟒:

>>> import ctypes
>>> ctypes.c_size_t(hash(b'abcd')).value
14608482441665817778
>>> getsizeof(ctypes.c_size_t(hash(b'abcd')).value)
36
>>> type(b'abcd')
<class 'bytes'>

rust eclipse :

use hashers::{builtin::DefaultHasher};
use std::hash::{Hash, Hasher};

pub fn hash_str(s: &str) -> u64 {
    let mut hasher = DefaultHasher::new();
    s.hash(&mut hasher);
    hasher.finish()
}

pub fn hash_bytes(b: &[u8]) -> u64 {
    let mut hasher = DefaultHasher::new();
    b.hash(&mut hasher);
    hasher.finish()
}

fn test_hash_str() {
    let s1: &str = "abcd";
    let h1: u64 = hash_str(s1);

    assert_eq!(h1, 13543138095457285553);
}
#[test]
fn test_hash_bytes() {
    let b1: &[u8] = "abcd".as_bytes();
    let h1: u64 = hash_bytes(b1);

    assert_eq!(h1, 18334232741324577590);
}

不幸的是,我不能在两端产生相同的值。有没有办法得到相同的值?
更新:
在签入Python实现之后,有一个细节是我最初忽略的,Python在每次运行时都使用一种随机盐,这意味着我从Python函数中得到的结果不可能与Rust版本相同。
可以使用PYTHONHASHSEED = 0 python禁用此功能...
然而,这仍然不能使Python产生与Rust版本相同的值,我在两端都尝试了定制的SipHash实现,结果是一致的:
两者都使用siphasher::sip::sipHasher13;和DefaultHasher产生相同的输出。String的结果与& str相同,但与. as_bytes()版本不同。

#[test]
    fn test_hash_string() {
        let s1: String = "abcd".to_string();
        let h1: u64 = hash_string(s1);

        assert_eq!(h1, 13543138095457285553);
    }

    #[test]
    fn test_hash_str() {
        let s1: &str = "abcd";
        let h1: u64 = hash_str(s1);

        assert_eq!(h1, 13543138095457285553);
    }
    #[test]
    fn test_hash_bytes() {
        let b1: &[u8] = "abcd".as_bytes();
        let h1: u64 = hash_bytes(b1);

        assert_eq!(h1, 18334232741324577590);
    }

在Python端禁用随机化后:

sh = SipHash(c=1, d=3)
    h = sh.auth(0, "abcd")
    assert h == 16416137402921954953
xdyibdwo

xdyibdwo1#

  • 不要将内部散列器用于外部目的。它并不意味着是可预测的或兼容的,它只意味着被用作内部散列。即使是mentions it in its docs

没有指定内部算法,因此不应在发布时依赖它及其散列。

  • 不要使用Rust的.hash()类型功能,它也不适用于外部哈希;它会对数据进行一些未指定的内部二进制化。使用hasher的.write功能直接向其提供二进制数据。

也就是说,解决方案是使用特定的散列库来实现兼容性,而不是使用内部散列库。
在Rust中,如果您需要siphash-1 - 3,这可能是siphasher。不过,我不确定Python,因为我有一段时间没有使用它了。
下面是Rust的示例代码:

use siphasher::sip::SipHasher13;

use std::hash::Hasher;

pub fn hash_str(s: &str) -> u64 {
    hash_bytes(s.as_bytes())
}

pub fn hash_bytes(b: &[u8]) -> u64 {
    let mut hasher = SipHasher13::new();
    hasher.write(b);
    hasher.finish()
}

#[test]
fn test_hash_str() {
    let s1: &str = "abcd";
    let h1: u64 = hash_str(s1);

    assert_eq!(h1, 16416137402921954953);
}

#[test]
fn test_hash_bytes() {
    let b1: &[u8] = "abcd".as_bytes();
    let h1: u64 = hash_bytes(b1);

    assert_eq!(h1, 16416137402921954953);
}

请注意,虽然我真的不推荐它,但Rust的内部hasher也是如此:
x一个一个一个一个x一个一个二个x

背景

那么,为什么s.hash()s.as_bytes().hash()的行为会很奇怪呢?
让我们编写一个简单的调试散列器:
一个三个三个一个
现在我们有了答案:

  • s似乎附加了0xff。这也可以在其源代码中看到:
fn write_str(&mut self, s: &str) {
    self.write(s.as_bytes());
    self.write_u8(0xff);
}
  • s.as_bytes()似乎在前面附加了奇怪的字节,在它的源代码中可以看到这是字符串的长度:
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_unstable(feature = "const_hash", issue = "104061")]
impl<T: ~const Hash> const Hash for [T] {
    #[inline]
    fn hash<H: ~const Hasher>(&self, state: &mut H) {
        state.write_length_prefix(self.len());
        Hash::hash_slice(self, state)
    }
}

相关问题