1use 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 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 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}