Skip to main content

clawft_kernel/
node_id.rs

1//! The one node-id derivation (ADR-103 D11, ADR-025).
2//!
3//! `node_id = hex(SHA-256(ed25519 pubkey)[..16])`: 32 lowercase hex
4//! characters. Every place that names a node (mesh handshake, cluster
5//! membership, heartbeats, chain event sources, status RPC, substrate
6//! `<node-id>` path segments, leaf registration) uses this value. Do
7//! not hash a pubkey into an id anywhere else.
8
9use sha2::{Digest, Sha256};
10
11/// Number of hash bytes kept in a node id (16 bytes = 32 hex chars).
12pub const NODE_ID_BYTES: usize = 16;
13
14/// Length of a node id in hex characters.
15pub const NODE_ID_HEX_LEN: usize = NODE_ID_BYTES * 2;
16
17/// Derive the node id from an Ed25519 public key.
18pub 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
29/// True when `s` has the shape of a node id produced by
30/// [`node_id_from_pubkey`] (32 lowercase hex characters).
31pub 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        // A child's node_id must equal its certified project_key_id.
50        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        // SHA-256 of 32 zero bytes starts 66687aadf862bd776c8fc18b8e9f8e20.
72        assert_eq!(
73            node_id_from_pubkey(&[0u8; 32]),
74            "66687aadf862bd776c8fc18b8e9f8e20"
75        );
76    }
77
78    /// Same vector as `clawft-mesh-local`; guards drift between the two
79    /// derivations.
80    #[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}