@@ -42,8 +42,10 @@ pub struct NodeFamiliesStorage<'a> {
4242 /// reserved as a "no family" sentinel.
4343 pub ( crate ) node_family_id_counter : Item < NodeFamilyId > ,
4444
45- /// All existing families, keyed by id, with a unique secondary index on
46- /// `owner` enforcing the **one-family-per-owner-address** invariant.
45+ /// All existing families, keyed by id, with unique secondary indexes on
46+ /// `owner` (one-family-per-owner-address) and on `name`
47+ /// (family names are globally unique — compared by raw bytes, so
48+ /// callers normalise upstream if case-insensitive uniqueness is desired).
4749 pub ( crate ) families : IndexedMap < NodeFamilyId , NodeFamily , NodeFamiliesIndex < ' a > > ,
4850
4951 /// Mapping from a node id to the family it currently belongs to. A node
@@ -178,14 +180,16 @@ impl<'a> NodeFamiliesStorage<'a> {
178180 ///
179181 /// The caller (a transaction handler) is responsible for:
180182 /// - validating `name`, `description` and `owner`;
183+ /// - normalising `name` (e.g. trim/lowercase) if case-insensitive
184+ /// uniqueness is desired — the storage layer compares raw bytes;
181185 /// - ensuring `owner` does not already own a family **and** is not
182186 /// currently a member of one.
183187 ///
184188 /// Returns the freshly persisted [`NodeFamily`]. The underlying
185- /// `IndexedMap` enforces the one-family-per-owner invariant via the
186- /// unique index on `owner` as a defence-in-depth check, so this call
187- /// will fail if `owner` already owns a family — but the caller must not
188- /// rely on it for the membership check.
189+ /// `IndexedMap` enforces the one-family-per-owner and unique-name
190+ /// invariants via unique indexes on `owner` and `name` as a
191+ /// defence-in-depth check, so this call will fail if either is already
192+ /// taken — but the caller must not rely on it for the membership check.
189193 pub ( crate ) fn register_new_family (
190194 & self ,
191195 store : & mut dyn Storage ,
@@ -673,6 +677,34 @@ mod tests {
673677 assert_eq ! ( f2. id, 2 ) ;
674678 }
675679
680+ #[ test]
681+ fn register_new_family_rejects_duplicate_name ( ) {
682+ let mut tester = init_contract_tester ( ) ;
683+ let s = NodeFamiliesStorage :: new ( ) ;
684+ let env = tester. env ( ) ;
685+ let alice = tester. addr_make ( "alice" ) ;
686+ let bob = tester. addr_make ( "bob" ) ;
687+
688+ s. register_new_family (
689+ tester. storage_mut ( ) ,
690+ & env,
691+ alice,
692+ "shared" . into ( ) ,
693+ "" . into ( ) ,
694+ )
695+ . unwrap ( ) ;
696+
697+ // unique-index defence-in-depth check
698+ let res = s. register_new_family (
699+ tester. storage_mut ( ) ,
700+ & env,
701+ bob,
702+ "shared" . into ( ) ,
703+ "" . into ( ) ,
704+ ) ;
705+ assert ! ( res. is_err( ) ) ;
706+ }
707+
676708 #[ test]
677709 fn register_new_family_rejects_duplicate_owner ( ) {
678710 let mut tester = init_contract_tester ( ) ;
0 commit comments