Skip to main content

darkpool_crypto/
kdf.rs

1//! Domain-separated key derivation for the Hisoka protocol.
2
3use ethers_core::types::U256;
4
5use crate::error::CryptoError;
6use crate::field::{poseidon_hash, string_to_fr};
7
8pub struct Kdf;
9
10impl Kdf {
11    /// Derive a child key: `Poseidon([master, stringToFr(purpose), nonce?])`.
12    /// Nonce is only included if non-zero. `purpose` must be <= 32 bytes.
13    pub fn derive(purpose: &str, master: U256, nonce: Option<U256>) -> Result<U256, CryptoError> {
14        let purpose_fr = string_to_fr(purpose)?;
15
16        Ok(match nonce {
17            Some(n) if !n.is_zero() => poseidon_hash(&[master, purpose_fr, n]),
18            _ => poseidon_hash(&[master, purpose_fr]),
19        })
20    }
21
22    /// Convenience wrapper: derive with a u64 index as nonce.
23    pub fn derive_indexed(purpose: &str, master: U256, index: u64) -> Result<U256, CryptoError> {
24        Self::derive(purpose, master, Some(U256::from(index)))
25    }
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    #[test]
33    fn test_kdf_deterministic() {
34        let master = U256::from(12345u64);
35        let result1 = Kdf::derive("hisoka.spend", master, None).unwrap();
36        let result2 = Kdf::derive("hisoka.spend", master, None).unwrap();
37
38        assert_eq!(result1, result2);
39        assert!(!result1.is_zero());
40    }
41
42    #[test]
43    fn test_kdf_different_purposes() {
44        let master = U256::from(12345u64);
45        let spend = Kdf::derive("hisoka.spend", master, None).unwrap();
46        let view = Kdf::derive("hisoka.view", master, None).unwrap();
47
48        assert_ne!(spend, view);
49    }
50
51    #[test]
52    fn test_kdf_with_nonce() {
53        let master = U256::from(12345u64);
54        let without_nonce = Kdf::derive("hisoka.eskTweak", master, None).unwrap();
55        let with_zero_nonce = Kdf::derive("hisoka.eskTweak", master, Some(U256::zero())).unwrap();
56        let with_nonce_1 = Kdf::derive("hisoka.eskTweak", master, Some(U256::from(1))).unwrap();
57        let with_nonce_2 = Kdf::derive("hisoka.eskTweak", master, Some(U256::from(2))).unwrap();
58
59        assert_eq!(without_nonce, with_zero_nonce);
60
61        assert_ne!(with_nonce_1, with_nonce_2);
62        assert_ne!(with_nonce_1, without_nonce);
63    }
64
65    #[test]
66    fn test_kdf_derive_indexed() {
67        let master = U256::from(12345u64);
68        let key_0 = Kdf::derive_indexed("hisoka.eskTweak", master, 0).unwrap();
69        let key_1 = Kdf::derive_indexed("hisoka.eskTweak", master, 1).unwrap();
70        let key_2 = Kdf::derive_indexed("hisoka.eskTweak", master, 2).unwrap();
71
72        let without_nonce = Kdf::derive("hisoka.eskTweak", master, None).unwrap();
73        assert_eq!(key_0, without_nonce);
74
75        assert_ne!(key_1, key_0);
76        assert_ne!(key_2, key_1);
77    }
78
79    #[test]
80    fn test_kdf_protocol_purposes() {
81        let master = U256::from(999999u64);
82
83        let purposes = [
84            "hisoka.spend",
85            "hisoka.view",
86            "hisoka.ivkMaster",
87            "hisoka.eskTweak",
88            "hisoka.ivkTweak",
89        ];
90
91        let mut results = Vec::new();
92        for purpose in purposes {
93            let result = Kdf::derive(purpose, master, None).unwrap();
94            assert!(!result.is_zero(), "{purpose} produced zero");
95            results.push(result);
96        }
97
98        for i in 0..results.len() {
99            for j in (i + 1)..results.len() {
100                assert_ne!(
101                    results[i], results[j],
102                    "{} and {} collided",
103                    purposes[i], purposes[j]
104                );
105            }
106        }
107    }
108
109    #[test]
110    fn test_kdf_rejects_oversized_purpose() {
111        let master = U256::from(12345u64);
112        let long_purpose = "a]".repeat(17);
113        let result = Kdf::derive(&long_purpose, master, None);
114        assert!(result.is_err());
115    }
116}