1use sha2::{Digest, Sha256};
10
11pub const NODE_ID_BYTES: usize = 16;
13
14pub const NODE_ID_HEX_LEN: usize = NODE_ID_BYTES * 2;
16
17pub fn node_id_from_pubkey(pubkey: &[u8; 32]) -> String {
19 use std::fmt::Write;
20 let hash = Sha256::digest(pubkey);
21 hash[..NODE_ID_BYTES]
22 .iter()
23 .fold(String::with_capacity(NODE_ID_HEX_LEN), |mut s, b| {
24 let _ = write!(s, "{b:02x}");
25 s
26 })
27}
28
29pub fn is_node_id(s: &str) -> bool {
32 s.len() == NODE_ID_HEX_LEN && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38
39 #[test]
40 fn stable_and_32_hex() {
41 let a = node_id_from_pubkey(&[7u8; 32]);
42 assert_eq!(a, node_id_from_pubkey(&[7u8; 32]));
43 assert_eq!(a.len(), 32);
44 assert!(is_node_id(&a));
45 }
46
47 #[test]
48 fn matches_the_project_key_id() {
49 for seed in [0u8, 2, 7, 255] {
51 let pk = ed25519_dalek::SigningKey::from_bytes(&[seed; 32])
52 .verifying_key()
53 .to_bytes();
54 assert_eq!(
55 node_id_from_pubkey(&pk),
56 clawft_types::project::cert::key_id(&pk)
57 );
58 }
59 }
60
61 #[test]
62 fn differs_per_key() {
63 assert_ne!(
64 node_id_from_pubkey(&[1u8; 32]),
65 node_id_from_pubkey(&[2u8; 32])
66 );
67 }
68
69 #[test]
70 fn matches_sha256_prefix_vector() {
71 assert_eq!(
73 node_id_from_pubkey(&[0u8; 32]),
74 "66687aadf862bd776c8fc18b8e9f8e20"
75 );
76 }
77
78 #[test]
81 fn golden_vector_pubkey_00_to_1f() {
82 let mut pk = [0u8; 32];
83 for (i, b) in pk.iter_mut().enumerate() {
84 *b = i as u8;
85 }
86 assert_eq!(node_id_from_pubkey(&pk), "630dcd2966c4336691125448bbb25b4f");
87 }
88
89 #[test]
90 fn shape_check_rejects_legacy_and_uuid() {
91 assert!(!is_node_id("n-3a7f9c"));
92 assert!(!is_node_id("550e8400-e29b-41d4-a716-446655440000"));
93 assert!(!is_node_id("66687AADF862BD776C8FC18B8E9F8E20"));
94 }
95}