@@ -8,6 +8,35 @@ use crate::handlers::{specialized_node_invite, tee_attestation_admission};
88use crate :: run:: NodeMode ;
99use crate :: NodeManager ;
1010
11+ /// Why a gossipsub topic was rejected as a `TeeAttestationAnnounce`
12+ /// namespace-governance topic.
13+ #[ derive( Debug , PartialEq , Eq ) ]
14+ enum NamespaceTopicError {
15+ /// Topic did not carry the `ns/` namespace-governance prefix. Fleet
16+ /// TEE nodes publish on `ns/<hex(namespace_id)>` via
17+ /// `NodeClient::publish_on_namespace`, so anything else is not an
18+ /// admission announce.
19+ NotNamespaceTopic ,
20+ /// Topic had the `ns/` prefix but the suffix was not a 32-byte hex id.
21+ MalformedHex ,
22+ }
23+
24+ /// Parse a `TeeAttestationAnnounce` gossipsub topic into its namespace id.
25+ ///
26+ /// Fleet TEE nodes announce on `ns/<hex(namespace_id)>` (the namespace
27+ /// governance topic — see `NodeClient::publish_on_namespace`,
28+ /// `governance_broadcast::ns_topic`, and the `ns/` handling in
29+ /// `subscriptions.rs`). The namespace IS its root group, so the returned
30+ /// 32-byte id is used directly as the admission group id.
31+ fn parse_namespace_announce_topic ( topic_str : & str ) -> Result < [ u8 ; 32 ] , NamespaceTopicError > {
32+ let hex = topic_str
33+ . strip_prefix ( "ns/" )
34+ . ok_or ( NamespaceTopicError :: NotNamespaceTopic ) ?;
35+ let mut bytes = [ 0u8 ; 32 ] ;
36+ hex:: decode_to_slice ( hex, & mut bytes) . map_err ( |_| NamespaceTopicError :: MalformedHex ) ?;
37+ Ok ( bytes)
38+ }
39+
1140pub ( super ) fn handle_specialized_broadcast (
1241 this : & mut NodeManager ,
1342 ctx : & mut actix:: Context < NodeManager > ,
@@ -81,24 +110,26 @@ pub(super) fn handle_specialized_broadcast(
81110 node_type : _,
82111 } => {
83112 let topic_str = topic. as_str ( ) ;
84- let group_id_bytes = match topic_str. strip_prefix ( "group/" ) {
85- Some ( hex) => {
86- let mut bytes = [ 0u8 ; 32 ] ;
87- if hex:: decode_to_slice ( hex, & mut bytes) . is_err ( ) {
88- warn ! (
89- %source,
90- topic = %topic_str,
91- "Invalid group topic hex in TeeAttestationAnnounce"
92- ) ;
93- return true ;
94- }
95- bytes
113+ // Fleet TEE nodes announce on the namespace governance topic
114+ // `ns/<hex(namespace_id)>` (see `NodeClient::publish_on_namespace`
115+ // and the `ns/` convention in `subscriptions.rs` /
116+ // `governance_broadcast::ns_topic`). The namespace IS its root
117+ // group, so the parsed namespace id is the admission group id.
118+ let namespace_id_bytes = match parse_namespace_announce_topic ( topic_str) {
119+ Ok ( bytes) => bytes,
120+ Err ( NamespaceTopicError :: MalformedHex ) => {
121+ warn ! (
122+ %source,
123+ topic = %topic_str,
124+ "Invalid namespace topic hex in TeeAttestationAnnounce"
125+ ) ;
126+ return true ;
96127 }
97- None => {
128+ Err ( NamespaceTopicError :: NotNamespaceTopic ) => {
98129 warn ! (
99130 %source,
100131 topic = %topic_str,
101- "TeeAttestationAnnounce received on non-group topic"
132+ "TeeAttestationAnnounce received on non-namespace topic"
102133 ) ;
103134 return true ;
104135 }
@@ -108,8 +139,8 @@ pub(super) fn handle_specialized_broadcast(
108139 %source,
109140 %public_key,
110141 nonce = %hex:: encode( * nonce) ,
111- group_id = %hex:: encode( group_id_bytes ) ,
112- "Received TEE attestation announce on group topic"
142+ namespace_id = %hex:: encode( namespace_id_bytes ) ,
143+ "Received TEE attestation announce on namespace topic"
113144 ) ;
114145
115146 let context_client = this. clients . context . clone ( ) ;
@@ -124,7 +155,7 @@ pub(super) fn handle_specialized_broadcast(
124155 quote_bytes,
125156 public_key,
126157 nonce,
127- group_id_bytes ,
158+ namespace_id_bytes ,
128159 )
129160 . await
130161 {
@@ -276,3 +307,69 @@ pub(super) fn handle_specialized_network_event(
276307 _ => false ,
277308 }
278309}
310+
311+ #[ cfg( test) ]
312+ mod tests {
313+ use super :: { parse_namespace_announce_topic, NamespaceTopicError } ;
314+
315+ /// Regression test for the `ns/` vs `group/` topic mismatch (PR #2096):
316+ /// fleet TEE nodes announce `TeeAttestationAnnounce` on
317+ /// `ns/<hex(namespace_id)>`, but the dispatcher used to strip
318+ /// `group/`, so the announce fell into the "non-namespace topic" arm
319+ /// and was dropped — `handle_tee_attestation_announce` / `admit_tee_node`
320+ /// never ran, and fleet TEE nodes were never admitted to the namespace
321+ /// group. The dispatcher must resolve an `ns/` topic to its namespace id
322+ /// and route it into the admission path.
323+ #[ test]
324+ fn ns_announce_topic_resolves_to_namespace_id_for_admission ( ) {
325+ let namespace_id = [ 0x42u8 ; 32 ] ;
326+ let topic = format ! ( "ns/{}" , hex:: encode( namespace_id) ) ;
327+
328+ let parsed = parse_namespace_announce_topic ( & topic)
329+ . expect ( "ns/<hex> announce topic must route into the admission path, not be dropped" ) ;
330+
331+ // The resolved id is what gets handed to
332+ // `handle_tee_attestation_announce` → `admit_tee_node` as the
333+ // admission group id (the namespace is its own root group).
334+ assert_eq ! ( parsed, namespace_id) ;
335+ }
336+
337+ /// The old (buggy) `group/<hex>` topic must NOT match this path anymore.
338+ /// `group/` is not how TEE announces are published (publish uses
339+ /// `publish_on_namespace` → `ns/`), so a `group/` topic here is a
340+ /// non-namespace topic and is correctly rejected rather than admitted.
341+ #[ test]
342+ fn legacy_group_topic_is_not_a_namespace_announce_topic ( ) {
343+ let topic = format ! ( "group/{}" , hex:: encode( [ 0x42u8 ; 32 ] ) ) ;
344+ assert_eq ! (
345+ parse_namespace_announce_topic( & topic) ,
346+ Err ( NamespaceTopicError :: NotNamespaceTopic ) ,
347+ ) ;
348+ }
349+
350+ /// A non-prefixed topic (e.g. a raw context id) is not a namespace
351+ /// announce topic.
352+ #[ test]
353+ fn unprefixed_topic_is_not_a_namespace_announce_topic ( ) {
354+ assert_eq ! (
355+ parse_namespace_announce_topic( "some-context-id" ) ,
356+ Err ( NamespaceTopicError :: NotNamespaceTopic ) ,
357+ ) ;
358+ }
359+
360+ /// An `ns/` topic with a malformed (non-hex / wrong-length) suffix is
361+ /// reported distinctly so the dispatcher can warn precisely instead of
362+ /// silently treating it as the wrong kind of topic.
363+ #[ test]
364+ fn ns_topic_with_malformed_hex_is_rejected_as_malformed ( ) {
365+ assert_eq ! (
366+ parse_namespace_announce_topic( "ns/not-hex" ) ,
367+ Err ( NamespaceTopicError :: MalformedHex ) ,
368+ ) ;
369+ // Right prefix, valid hex, wrong length (16 bytes, not 32).
370+ assert_eq ! (
371+ parse_namespace_announce_topic( & format!( "ns/{}" , hex:: encode( [ 0u8 ; 16 ] ) ) ) ,
372+ Err ( NamespaceTopicError :: MalformedHex ) ,
373+ ) ;
374+ }
375+ }
0 commit comments