Skip to main content

host_encoding/
identity.rs

1//! Pure encoding and decoding for the Identity and Resources pallets on
2//! People chains.
3//!
4//! No I/O. No network calls. WASM-safe.
5//!
6//! Provides username normalization, storage key derivation for
7//! `Identity::UsernameInfoOf` and `Resources::Consumers`, and SCALE
8//! decoding of the resulting storage values.
9
10use thiserror::Error;
11
12// Re-export shared hex utilities from the crate root.
13pub use crate::{hex_decode, hex_encode};
14
15// Import compact-length encoder from the dotns module — avoids duplicating the
16// SCALE logic that already lives there.
17use crate::dotns::scale_compact_len;
18
19/// Errors produced by the identity encoding layer.
20#[derive(Debug, Error, PartialEq)]
21pub enum IdentityError {
22    /// The normalised username exceeds the 32-byte on-chain limit.
23    #[error("username is too long: {len} bytes (max 32)")]
24    UsernameTooLong { len: usize },
25
26    /// An empty string was supplied.
27    #[error("username must not be empty")]
28    UsernameEmpty,
29
30    /// A character that is not `[a-z0-9.]` was found after ASCII lowercasing.
31    #[error("username contains invalid character: {ch:?}")]
32    InvalidCharacter { ch: char },
33
34    /// The dot placement is invalid (leading, trailing, or consecutive dots).
35    #[error("username has invalid dot placement: {reason}")]
36    InvalidDotPlacement { reason: &'static str },
37
38    /// The storage response could not be decoded.
39    #[error("invalid response: {msg}")]
40    InvalidResponse { msg: String },
41}
42
43// ---------------------------------------------------------------------------
44// Public types
45// ---------------------------------------------------------------------------
46
47/// Decoded identity information from `Resources::Consumers`.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ConsumerInfo {
50    /// 65-byte P-256 public key (uncompressed SEC1 format).
51    pub identifier_key: Vec<u8>,
52    /// Full person username, present only for Person-level credibility.
53    pub full_username: Option<String>,
54    /// Lite username (always present).
55    pub lite_username: String,
56    /// Credibility level for this consumer.
57    pub credibility: Credibility,
58}
59
60/// Credibility level stored in `Resources::Consumers`.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum Credibility {
63    /// Lite credibility — no alias or timestamp.
64    Lite,
65    /// Person-level credibility with a 32-byte alias and last-update timestamp.
66    Person {
67        /// 32-byte opaque alias identifier.
68        alias: [u8; 32],
69        /// UNIX timestamp (seconds) of the last credibility update.
70        last_update: u64,
71    },
72}
73
74// ---------------------------------------------------------------------------
75// Public API
76// ---------------------------------------------------------------------------
77
78/// Normalise a username and return its UTF-8 bytes.
79///
80/// Applies ASCII lowercasing (not Unicode lowercasing, to avoid the Turkish İ
81/// problem) and validates that every character is in `[a-z0-9.]`.  The
82/// normalised form must be 1–32 bytes inclusive.
83///
84/// Dot placement rules (enforced after character validation):
85/// - No leading dot (`".alice"` is rejected).
86/// - No trailing dot (`"alice."` is rejected).
87/// - No consecutive dots (`"alice..1"` is rejected).
88///
89/// These rules match the on-chain People pallet's username validity checks and
90/// prevent storage key confusion from structurally malformed names.
91pub fn normalize_username(username: &str) -> Result<Vec<u8>, IdentityError> {
92    if username.is_empty() {
93        return Err(IdentityError::UsernameEmpty);
94    }
95
96    // Reject null bytes before any further processing — they would pass the
97    // `is_ascii()` check on some platforms and corrupt storage keys.
98    if username.contains('\0') {
99        return Err(IdentityError::InvalidCharacter { ch: '\0' });
100    }
101
102    // ASCII-only lowercase to sidestep Unicode case-folding quirks (Turkish İ,
103    // Greek ς, etc.).
104    let lower = username.to_ascii_lowercase();
105    let bytes = lower.as_bytes();
106
107    // Validate characters after lowercasing.
108    for &b in bytes {
109        let ch = b as char;
110        if !matches!(b, b'a'..=b'z' | b'0'..=b'9' | b'.') {
111            return Err(IdentityError::InvalidCharacter { ch });
112        }
113    }
114
115    if bytes.len() > 32 {
116        return Err(IdentityError::UsernameTooLong { len: bytes.len() });
117    }
118
119    // Reject structurally invalid dot placements. Character validation above
120    // has already confirmed every byte is in [a-z0-9.], so these checks are
121    // purely positional.
122    if bytes.first() == Some(&b'.') {
123        return Err(IdentityError::InvalidDotPlacement {
124            reason: "leading dot",
125        });
126    }
127    if bytes.last() == Some(&b'.') {
128        return Err(IdentityError::InvalidDotPlacement {
129            reason: "trailing dot",
130        });
131    }
132    if bytes.windows(2).any(|w| w == b"..") {
133        return Err(IdentityError::InvalidDotPlacement {
134            reason: "consecutive dots",
135        });
136    }
137
138    Ok(bytes.to_vec())
139}
140
141/// Derive the Substrate storage key for `Identity::UsernameInfoOf[username]`.
142///
143/// Layout:
144/// ```text
145/// twox_128("Identity") ++ twox_128("UsernameInfoOf") ++ blake2_128_concat(scale_encode(username_bytes))
146/// ```
147///
148/// The username is SCALE-encoded as `BoundedVec<u8>`:
149/// `compact_len(bytes.len()) ++ bytes`.
150///
151/// `username_bytes` must already be normalised (i.e. the output of
152/// [`normalize_username`]).
153pub fn username_info_of_key(username_bytes: &[u8]) -> Vec<u8> {
154    let pallet_hash = twox_128(b"Identity");
155    let storage_hash = twox_128(b"UsernameInfoOf");
156
157    // SCALE BoundedVec<u8>: compact length prefix followed by the raw bytes.
158    let mut encoded_username = Vec::with_capacity(username_bytes.len() + 4);
159    // scale_compact_len cannot fail for inputs <= 32 bytes (well within the
160    // single-byte mode limit of 63).
161    scale_compact_len(&mut encoded_username, username_bytes.len())
162        .expect("username_bytes.len() <= 32 is always within compact single-byte range");
163    encoded_username.extend_from_slice(username_bytes);
164
165    // blake2_128_concat = blake2_128(data) ++ data
166    let hash_prefix = blake2_128(&encoded_username);
167
168    let mut key = Vec::with_capacity(16 + 16 + 16 + encoded_username.len());
169    key.extend_from_slice(&pallet_hash);
170    key.extend_from_slice(&storage_hash);
171    key.extend_from_slice(&hash_prefix);
172    key.extend_from_slice(&encoded_username);
173    key
174}
175
176/// Derive the Substrate storage key for `Resources::UsernameOwnerOf[username]`.
177///
178/// Used on the Individuality chain where lite usernames are stored in the
179/// `Resources` pallet rather than `Identity`.
180///
181/// Layout:
182/// ```text
183/// twox_128("Resources") ++ twox_128("UsernameOwnerOf") ++ blake2_128_concat(scale_encode(username_bytes))
184/// ```
185///
186/// The username is SCALE-encoded as `BoundedVec<u8>`:
187/// `compact_len(bytes.len()) ++ bytes`.
188///
189/// `username_bytes` must already be normalised (i.e. the output of
190/// [`normalize_username`]).
191pub fn username_owner_of_key(username_bytes: &[u8]) -> Vec<u8> {
192    let pallet_hash = twox_128(b"Resources");
193    let storage_hash = twox_128(b"UsernameOwnerOf");
194
195    // SCALE BoundedVec<u8>: compact length prefix followed by the raw bytes.
196    let mut encoded_username = Vec::with_capacity(username_bytes.len() + 4);
197    // scale_compact_len cannot fail for inputs <= 32 bytes (well within the
198    // single-byte mode limit of 63).
199    scale_compact_len(&mut encoded_username, username_bytes.len())
200        .expect("username_bytes.len() <= 32 is always within compact single-byte range");
201    encoded_username.extend_from_slice(username_bytes);
202
203    // blake2_128_concat = blake2_128(data) ++ data
204    let hash_prefix = blake2_128(&encoded_username);
205
206    let mut key = Vec::with_capacity(16 + 16 + 16 + encoded_username.len());
207    key.extend_from_slice(&pallet_hash);
208    key.extend_from_slice(&storage_hash);
209    key.extend_from_slice(&hash_prefix);
210    key.extend_from_slice(&encoded_username);
211    key
212}
213
214/// Decode the raw `AccountId32` returned by `Resources::UsernameOwnerOf` on the
215/// Individuality chain.
216///
217/// Unlike `Identity::UsernameInfoOf`, this storage map returns a bare
218/// `AccountId32` — exactly 32 bytes with no Option wrapper and no provider byte.
219///
220/// | Input                        | Output               |
221/// |------------------------------|----------------------|
222/// | `[]` (empty — slot absent)   | `Ok(None)`           |
223/// | `[b0..b31]` (32 bytes)       | `Ok(Some([u8; 32]))` |
224/// | `< 32 bytes`                 | `Err`                |
225///
226/// Trailing bytes beyond 32 are silently ignored (same permissive behaviour as
227/// `decode_username_info_owner`).
228pub fn decode_username_owner(data: &[u8]) -> Result<Option<[u8; 32]>, IdentityError> {
229    if data.is_empty() {
230        return Ok(None);
231    }
232    if data.len() < 32 {
233        return Err(IdentityError::InvalidResponse {
234            msg: format!(
235                "UsernameOwnerOf truncated: expected 32 bytes, got {}",
236                data.len()
237            ),
238        });
239    }
240    let mut account = [0u8; 32];
241    account.copy_from_slice(&data[..32]);
242    Ok(Some(account))
243}
244
245/// Derive the Substrate storage key for `Resources::Consumers[account_id]`.
246///
247/// Layout:
248/// ```text
249/// twox_128("Resources") ++ twox_128("Consumers") ++ blake2_128_concat(account_id)
250/// ```
251///
252/// `account_id` is a fixed-size 32-byte type — it is passed directly to
253/// `blake2_128_concat` WITHOUT a SCALE compact length prefix (unlike
254/// `BoundedVec<u8>` keys such as usernames).
255pub fn consumers_key(account_id: &[u8; 32]) -> Vec<u8> {
256    let pallet_hash = twox_128(b"Resources");
257    let storage_hash = twox_128(b"Consumers");
258
259    // blake2_128_concat over the raw 32-byte account ID — no length prefix.
260    let hash_prefix = blake2_128(account_id);
261
262    let mut key = Vec::with_capacity(16 + 16 + 16 + 32);
263    key.extend_from_slice(&pallet_hash);
264    key.extend_from_slice(&storage_hash);
265    key.extend_from_slice(&hash_prefix);
266    key.extend_from_slice(account_id);
267    key
268}
269
270/// Decode the SCALE-encoded `Option<AccountId32>` returned by a storage query.
271///
272/// | Input                        | Output            |
273/// |------------------------------|-------------------|
274/// | `[]` (empty — slot absent)   | `Ok(None)`        |
275/// | `[0x00]`                     | `Ok(None)`        |
276/// | `[0x01, b0..b31]`            | `Ok(Some([u8;32]))`|
277///
278/// Returns `Err` for truncated data, unknown tag bytes, or trailing garbage.
279pub fn decode_option_account_id(data: &[u8]) -> Result<Option<[u8; 32]>, IdentityError> {
280    // Empty response — the storage slot doesn't exist on-chain.
281    if data.is_empty() {
282        return Ok(None);
283    }
284
285    match data[0] {
286        0x00 => {
287            // None variant — reject trailing bytes to catch malformed responses.
288            if data.len() != 1 {
289                return Err(IdentityError::InvalidResponse {
290                    msg: format!(
291                        "Option::None tag followed by {} unexpected byte(s)",
292                        data.len() - 1
293                    ),
294                });
295            }
296            Ok(None)
297        }
298        0x01 => {
299            // Some variant — expect exactly 32 bytes of AccountId32 payload.
300            let payload = &data[1..];
301            if payload.len() < 32 {
302                return Err(IdentityError::InvalidResponse {
303                    msg: format!(
304                        "Option::Some truncated: expected 32 bytes, got {}",
305                        payload.len()
306                    ),
307                });
308            }
309            if payload.len() > 32 {
310                return Err(IdentityError::InvalidResponse {
311                    msg: format!(
312                        "Option::Some has {} trailing byte(s) after AccountId32",
313                        payload.len() - 32
314                    ),
315                });
316            }
317            let mut account = [0u8; 32];
318            account.copy_from_slice(payload);
319            Ok(Some(account))
320        }
321        tag => Err(IdentityError::InvalidResponse {
322            msg: format!("unknown Option tag byte: 0x{tag:02x}"),
323        }),
324    }
325}
326
327/// Decode the raw `UsernameInformation` value from `Identity::UsernameInfoOf`.
328///
329/// The value is `{ owner: AccountId32, provider: u8 }` — 33 bytes minimum.
330/// We extract only the `owner` field (first 32 bytes).
331///
332/// Returns `Ok(Some([u8; 32]))` when data is present, `Ok(None)` for empty input.
333pub fn decode_username_info_owner(data: &[u8]) -> Result<Option<[u8; 32]>, IdentityError> {
334    if data.is_empty() {
335        return Ok(None);
336    }
337    if data.len() < 32 {
338        return Err(IdentityError::InvalidResponse {
339            msg: format!(
340                "UsernameInformation truncated: expected >= 32 bytes, got {}",
341                data.len()
342            ),
343        });
344    }
345    let mut account = [0u8; 32];
346    account.copy_from_slice(&data[..32]);
347    Ok(Some(account))
348}
349
350/// Decode a SCALE-encoded `ConsumerInfo` from a `state_getStorage` response.
351///
352/// SCALE layout (in field order):
353/// 1. `identifier_key`: 65 bytes (fixed array, no length prefix)
354/// 2. `full_username`: `Option<BoundedVec<u8, 32>>` — `0x00` = None, `0x01 + compact_len + bytes` = Some
355/// 3. `lite_username`: `BoundedVec<u8, 32>` — compact_len + bytes
356/// 4. `credibility`: enum — `0x00` = Lite, `0x01 + alias(32B) + last_update(u64 LE)` = Person
357///
358/// Fields after `credibility` (e.g. `stmt_store_slots`) are intentionally
359/// skipped — clients do not use them.
360///
361/// Returns `Err` for empty input, truncated data, invalid UTF-8, or unknown
362/// enum tags.
363pub fn decode_consumer_info(data: &[u8]) -> Result<ConsumerInfo, IdentityError> {
364    if data.is_empty() {
365        return Err(IdentityError::InvalidResponse {
366            msg: "empty data — storage slot absent".into(),
367        });
368    }
369
370    let mut cursor = 0usize;
371
372    // 1. identifier_key: fixed 65 bytes.
373    let identifier_key = read_fixed(data, &mut cursor, 65)?;
374
375    // 2. full_username: Option<BoundedVec<u8, 32>>.
376    let full_username = decode_option_bounded_vec_utf8(data, &mut cursor, "full_username")?;
377
378    // 3. lite_username: BoundedVec<u8, 32> (always present).
379    let lite_username = decode_bounded_vec_utf8(data, &mut cursor, "lite_username")?;
380
381    // 4. credibility enum.
382    let credibility = decode_credibility(data, &mut cursor)?;
383
384    Ok(ConsumerInfo {
385        identifier_key,
386        full_username,
387        lite_username,
388        credibility,
389    })
390}
391
392/// Format an AccountId32 as a `0x`-prefixed lowercase hex string.
393pub fn account_id_to_hex(account_id: &[u8; 32]) -> String {
394    hex_encode(account_id)
395}
396
397// ---------------------------------------------------------------------------
398// Private decoding helpers
399// ---------------------------------------------------------------------------
400
401/// Read exactly `n` bytes from `data` starting at `*cursor`, advance the cursor.
402fn read_fixed(data: &[u8], cursor: &mut usize, n: usize) -> Result<Vec<u8>, IdentityError> {
403    let end = cursor
404        .checked_add(n)
405        .ok_or_else(|| IdentityError::InvalidResponse {
406            msg: "byte offset overflow".into(),
407        })?;
408    if end > data.len() {
409        return Err(IdentityError::InvalidResponse {
410            msg: format!(
411                "truncated: need {} bytes at offset {}, only {} available",
412                n,
413                *cursor,
414                data.len() - *cursor
415            ),
416        });
417    }
418    let bytes = data[*cursor..end].to_vec();
419    *cursor = end;
420    Ok(bytes)
421}
422
423/// Decode a SCALE compact integer from `data` at `*cursor`, advance the cursor.
424///
425/// Only the single-byte (0–63) and two-byte (64–16383) modes are needed for
426/// BoundedVec<u8, 32> fields (max length 32 fits in single-byte mode).
427fn read_compact(data: &[u8], cursor: &mut usize, field: &str) -> Result<usize, IdentityError> {
428    if *cursor >= data.len() {
429        return Err(IdentityError::InvalidResponse {
430            msg: format!("truncated: missing compact length byte for {field}"),
431        });
432    }
433    let first = data[*cursor];
434    let mode = first & 0b11;
435    match mode {
436        0b00 => {
437            // Single-byte mode: value in top 6 bits.
438            *cursor += 1;
439            Ok((first >> 2) as usize)
440        }
441        0b01 => {
442            // Two-byte mode: 14-bit value across two bytes.
443            if *cursor + 2 > data.len() {
444                return Err(IdentityError::InvalidResponse {
445                    msg: format!("truncated: 2-byte compact integer for {field}"),
446                });
447            }
448            let second = data[*cursor + 1];
449            let value = first as usize >> 2 | (second as usize) << 6;
450            *cursor += 2;
451            Ok(value)
452        }
453        _ => Err(IdentityError::InvalidResponse {
454            msg: format!("unsupported compact mode 0b{mode:02b} for {field}"),
455        }),
456    }
457}
458
459/// Decode a `BoundedVec<u8>` as a UTF-8 string.
460fn decode_bounded_vec_utf8(
461    data: &[u8],
462    cursor: &mut usize,
463    field: &str,
464) -> Result<String, IdentityError> {
465    let len = read_compact(data, cursor, field)?;
466    let raw = read_fixed(data, cursor, len)?;
467    String::from_utf8(raw).map_err(|_| IdentityError::InvalidResponse {
468        msg: format!("{field} contains invalid UTF-8"),
469    })
470}
471
472/// Decode an `Option<BoundedVec<u8>>` as an optional UTF-8 string.
473fn decode_option_bounded_vec_utf8(
474    data: &[u8],
475    cursor: &mut usize,
476    field: &str,
477) -> Result<Option<String>, IdentityError> {
478    if *cursor >= data.len() {
479        return Err(IdentityError::InvalidResponse {
480            msg: format!("truncated: missing Option tag for {field}"),
481        });
482    }
483    let tag = data[*cursor];
484    *cursor += 1;
485    match tag {
486        0x00 => Ok(None),
487        0x01 => Ok(Some(decode_bounded_vec_utf8(data, cursor, field)?)),
488        _ => Err(IdentityError::InvalidResponse {
489            msg: format!("unknown Option tag 0x{tag:02x} for {field}"),
490        }),
491    }
492}
493
494/// Decode the `Credibility` enum at `*cursor`.
495fn decode_credibility(data: &[u8], cursor: &mut usize) -> Result<Credibility, IdentityError> {
496    if *cursor >= data.len() {
497        return Err(IdentityError::InvalidResponse {
498            msg: "truncated: missing credibility tag byte".into(),
499        });
500    }
501    let tag = data[*cursor];
502    *cursor += 1;
503    match tag {
504        0x00 => Ok(Credibility::Lite),
505        0x01 => {
506            // Person variant: 32-byte alias + u64 LE last_update.
507            let alias_bytes = read_fixed(data, cursor, 32)?;
508            let ts_bytes = read_fixed(data, cursor, 8)?;
509
510            let mut alias = [0u8; 32];
511            alias.copy_from_slice(&alias_bytes);
512
513            let last_update = u64::from_le_bytes(
514                ts_bytes
515                    .as_slice()
516                    .try_into()
517                    .expect("ts_bytes is exactly 8 bytes from read_fixed"),
518            );
519            Ok(Credibility::Person { alias, last_update })
520        }
521        tag => Err(IdentityError::InvalidResponse {
522            msg: format!("unknown Credibility variant tag: 0x{tag:02x}"),
523        }),
524    }
525}
526
527// ---------------------------------------------------------------------------
528// Private hash helpers
529// ---------------------------------------------------------------------------
530
531/// XxHash-based 128-bit hash used by Substrate for pallet/storage name hashing.
532///
533/// Computes two independent XxHash64 values (seed 0 and seed 1) and
534/// concatenates their little-endian bytes, matching the Substrate
535/// `twox_128` implementation exactly.
536fn twox_128(data: &[u8]) -> [u8; 16] {
537    use std::hash::Hasher;
538    use twox_hash::XxHash64;
539
540    let mut h0 = XxHash64::with_seed(0);
541    h0.write(data);
542    let mut h1 = XxHash64::with_seed(1);
543    h1.write(data);
544
545    let mut result = [0u8; 16];
546    result[..8].copy_from_slice(&h0.finish().to_le_bytes());
547    result[8..].copy_from_slice(&h1.finish().to_le_bytes());
548    result
549}
550
551/// Blake2b with a 16-byte (128-bit) output digest.
552fn blake2_128(data: &[u8]) -> [u8; 16] {
553    use blake2::digest::consts::U16;
554    use blake2::{Blake2b, Digest};
555
556    let mut hasher = Blake2b::<U16>::new();
557    hasher.update(data);
558    let result = hasher.finalize();
559    let mut out = [0u8; 16];
560    out.copy_from_slice(&result);
561    out
562}
563
564// ---------------------------------------------------------------------------
565// Tests
566// ---------------------------------------------------------------------------
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571
572    // -----------------------------------------------------------------------
573    // normalize_username
574    // -----------------------------------------------------------------------
575
576    #[test]
577    fn test_rejects_empty_username() {
578        assert_eq!(normalize_username(""), Err(IdentityError::UsernameEmpty));
579    }
580
581    #[test]
582    fn test_accepts_single_char_username() {
583        assert_eq!(normalize_username("a"), Ok(b"a".to_vec()));
584    }
585
586    #[test]
587    fn test_accepts_max_length_username() {
588        // Exactly 32 lowercase ASCII characters — the on-chain limit.
589        let username = "a".repeat(32);
590        let result = normalize_username(&username).unwrap();
591        assert_eq!(result.len(), 32);
592    }
593
594    #[test]
595    fn test_rejects_username_one_over_max_length() {
596        let username = "a".repeat(33);
597        assert_eq!(
598            normalize_username(&username),
599            Err(IdentityError::UsernameTooLong { len: 33 })
600        );
601    }
602
603    #[test]
604    fn test_normalizes_mixed_case_username() {
605        let result = normalize_username("Alice").unwrap();
606        assert_eq!(result, b"alice");
607    }
608
609    #[test]
610    fn test_accepts_already_lowercase_username() {
611        let result = normalize_username("alice").unwrap();
612        assert_eq!(result, b"alice");
613    }
614
615    #[test]
616    fn test_rejects_space_character() {
617        assert_eq!(
618            normalize_username("ali ce"),
619            Err(IdentityError::InvalidCharacter { ch: ' ' })
620        );
621    }
622
623    #[test]
624    fn test_rejects_tab_character() {
625        assert_eq!(
626            normalize_username("ali\tce"),
627            Err(IdentityError::InvalidCharacter { ch: '\t' })
628        );
629    }
630
631    #[test]
632    fn test_rejects_null_byte() {
633        assert_eq!(
634            normalize_username("ali\0ce"),
635            Err(IdentityError::InvalidCharacter { ch: '\0' })
636        );
637    }
638
639    #[test]
640    fn test_rejects_slash_character() {
641        assert_eq!(
642            normalize_username("ali/ce"),
643            Err(IdentityError::InvalidCharacter { ch: '/' })
644        );
645    }
646
647    #[test]
648    fn test_rejects_exclamation_character() {
649        assert_eq!(
650            normalize_username("alice!"),
651            Err(IdentityError::InvalidCharacter { ch: '!' })
652        );
653    }
654
655    #[test]
656    fn test_accepts_username_with_internal_dot() {
657        // A dot between alphanumeric segments is valid (e.g. on-chain handles).
658        let result = normalize_username("alice.dot").unwrap();
659        assert_eq!(result, b"alice.dot");
660    }
661
662    #[test]
663    fn test_rejects_username_with_leading_dot() {
664        assert_eq!(
665            normalize_username(".alice"),
666            Err(IdentityError::InvalidDotPlacement {
667                reason: "leading dot"
668            })
669        );
670    }
671
672    #[test]
673    fn test_rejects_username_with_trailing_dot() {
674        assert_eq!(
675            normalize_username("alice."),
676            Err(IdentityError::InvalidDotPlacement {
677                reason: "trailing dot"
678            })
679        );
680    }
681
682    #[test]
683    fn test_rejects_username_with_consecutive_dots() {
684        assert_eq!(
685            normalize_username("alice..1"),
686            Err(IdentityError::InvalidDotPlacement {
687                reason: "consecutive dots"
688            })
689        );
690    }
691
692    #[test]
693    fn test_rejects_username_with_only_dots() {
694        // A single dot is both a leading and trailing dot — rejected as leading dot first.
695        assert_eq!(
696            normalize_username("."),
697            Err(IdentityError::InvalidDotPlacement {
698                reason: "leading dot"
699            })
700        );
701    }
702
703    #[test]
704    fn test_accepts_alphanumeric_username() {
705        let result = normalize_username("user42").unwrap();
706        assert_eq!(result, b"user42");
707    }
708
709    // -----------------------------------------------------------------------
710    // username_info_of_key
711    // -----------------------------------------------------------------------
712
713    /// The storage key is 32 (pallet+storage hashes) + 16 (blake2_128 prefix) +
714    /// compact_len(n) + n bytes for the username.  For a 1-byte username the
715    /// compact length is 1 byte, so total = 32 + 16 + 1 + 1 = 50.
716    #[test]
717    fn test_username_info_of_key_length_single_byte_username() {
718        let key = username_info_of_key(b"a");
719        // 16 (pallet) + 16 (storage) + 16 (blake2_128) + 1 (compact len) + 1 (byte) = 50
720        assert_eq!(key.len(), 50);
721    }
722
723    #[test]
724    fn test_username_info_of_key_length_longer_username() {
725        // "alice.dot" = 9 bytes; compact(9) = 1 byte → total = 58
726        let key = username_info_of_key(b"alice.dot");
727        assert_eq!(key.len(), 16 + 16 + 16 + 1 + 9);
728    }
729
730    /// The first 32 bytes of the storage key are fixed for any username queried
731    /// against the same pallet/storage entry.
732    #[test]
733    fn test_username_info_of_key_prefix_is_stable() {
734        let key_a = username_info_of_key(b"alice");
735        let key_b = username_info_of_key(b"bob");
736        // First 32 bytes = twox_128("Identity") ++ twox_128("UsernameInfoOf")
737        assert_eq!(&key_a[..32], &key_b[..32]);
738    }
739
740    #[test]
741    fn test_different_usernames_produce_different_storage_keys() {
742        let key_a = username_info_of_key(b"alice");
743        let key_b = username_info_of_key(b"bob");
744        assert_ne!(key_a, key_b);
745    }
746
747    /// Normalised lowercase and the equivalent mixed-case input must produce
748    /// identical keys (the caller is responsible for normalising first, but
749    /// we verify that the byte-level encoding is stable).
750    #[test]
751    fn test_username_info_of_key_same_for_lowercase_and_normalised_input() {
752        let lower = normalize_username("Alice").unwrap();
753        let direct = normalize_username("alice").unwrap();
754        assert_eq!(username_info_of_key(&lower), username_info_of_key(&direct));
755    }
756
757    /// Pinned vector: the first 32 bytes of the storage key are derived from
758    /// `twox_128("Identity")` and `twox_128("UsernameInfoOf")`.  This value
759    /// must not change without a corresponding on-chain storage migration.
760    #[test]
761    fn test_username_info_of_key_prefix_pinned() {
762        let key = username_info_of_key(b"a");
763        // twox_128("Identity") ++ twox_128("UsernameInfoOf") — computed once
764        // from the twox_128 implementation in this crate and pinned here so
765        // any accidental pallet rename is caught immediately.
766        let identity_hash = hex_decode(&hex_encode(&twox_128(b"Identity"))).unwrap();
767        let storage_hash = hex_decode(&hex_encode(&twox_128(b"UsernameInfoOf"))).unwrap();
768        let mut expected_prefix = Vec::new();
769        expected_prefix.extend_from_slice(&identity_hash);
770        expected_prefix.extend_from_slice(&storage_hash);
771        assert_eq!(&key[..32], expected_prefix.as_slice());
772    }
773
774    /// Pinned against the known live-chain value for `Identity::UsernameInfoOf`.
775    ///
776    /// The prefix `0x2aeddc77fe58c98d50bd37f1b90840f93da3e15a0621aae33d5d5d4a5487e798`
777    /// was observed from the live Paseo People chain and is pinned here to catch
778    /// any accidental regression in the pallet or storage name.
779    #[test]
780    fn test_username_info_of_key_prefix_matches_live_chain() {
781        let key = username_info_of_key(b"a");
782        let expected_hex = "2aeddc77fe58c98d50bd37f1b90840f93da3e15a0621aae33d5d5d4a5487e798";
783        let expected_bytes = hex_decode(&format!("0x{expected_hex}")).unwrap();
784        assert_eq!(&key[..32], expected_bytes.as_slice());
785    }
786
787    // -----------------------------------------------------------------------
788    // username_owner_of_key
789    // -----------------------------------------------------------------------
790
791    /// Pin the first 32 bytes of the storage key for `Resources::UsernameOwnerOf`.
792    ///
793    /// These bytes are derived from `twox_128("Resources") ++ twox_128("UsernameOwnerOf")`
794    /// and must remain stable unless the Individuality chain undergoes a storage
795    /// migration that renames the pallet or storage item.
796    #[test]
797    fn test_username_owner_of_key_prefix_pinned() {
798        let key = username_owner_of_key(b"a");
799        let resources_hash = twox_128(b"Resources");
800        let storage_hash = twox_128(b"UsernameOwnerOf");
801        let mut expected_prefix = Vec::new();
802        expected_prefix.extend_from_slice(&resources_hash);
803        expected_prefix.extend_from_slice(&storage_hash);
804        assert_eq!(&key[..32], expected_prefix.as_slice());
805    }
806
807    /// The same username must produce different storage keys for
808    /// `Identity::UsernameInfoOf` vs `Resources::UsernameOwnerOf`.
809    #[test]
810    fn test_username_owner_of_key_differs_from_info_key() {
811        let username = b"alice";
812        let info_key = username_info_of_key(username);
813        let owner_key = username_owner_of_key(username);
814        // Different pallet/storage prefix means the keys must diverge.
815        assert_ne!(info_key, owner_key);
816        // The suffix (blake2_128_concat of the encoded username) is identical;
817        // only the first 32 bytes differ.
818        assert_ne!(&info_key[..32], &owner_key[..32]);
819    }
820
821    // -----------------------------------------------------------------------
822    // decode_username_owner
823    // -----------------------------------------------------------------------
824
825    /// A valid 32-byte response decodes to `Some(account)`.
826    #[test]
827    fn test_decode_username_owner_valid() {
828        let account = [0x42u8; 32];
829        assert_eq!(decode_username_owner(&account), Ok(Some(account)));
830    }
831
832    /// An empty slice (storage slot absent) decodes to `None`.
833    #[test]
834    fn test_decode_username_owner_empty() {
835        assert_eq!(decode_username_owner(&[]), Ok(None));
836    }
837
838    /// A slice shorter than 32 bytes is an error.
839    #[test]
840    fn test_decode_username_owner_truncated() {
841        let data = vec![0x42u8; 10];
842        assert!(matches!(
843            decode_username_owner(&data),
844            Err(IdentityError::InvalidResponse { .. })
845        ));
846    }
847
848    // -----------------------------------------------------------------------
849    // consumers_key
850    // -----------------------------------------------------------------------
851
852    #[test]
853    fn test_consumers_key_has_correct_length() {
854        let account_id = [0x42u8; 32];
855        let key = consumers_key(&account_id);
856        // 16 (pallet) + 16 (storage) + 16 (blake2_128) + 32 (account_id) = 80
857        assert_eq!(key.len(), 80);
858    }
859
860    #[test]
861    fn test_consumers_key_prefix_is_stable() {
862        let key_a = consumers_key(&[0x01u8; 32]);
863        let key_b = consumers_key(&[0x02u8; 32]);
864        // First 32 bytes = twox_128("Resources") ++ twox_128("Consumers")
865        assert_eq!(&key_a[..32], &key_b[..32]);
866    }
867
868    #[test]
869    fn test_consumers_key_differs_for_different_account_ids() {
870        let key_a = consumers_key(&[0x01u8; 32]);
871        let key_b = consumers_key(&[0x02u8; 32]);
872        assert_ne!(key_a, key_b);
873    }
874
875    #[test]
876    fn test_consumers_key_prefix_uses_resources_pallet() {
877        let key = consumers_key(&[0x00u8; 32]);
878        let resources_hash = twox_128(b"Resources");
879        let consumers_hash = twox_128(b"Consumers");
880        assert_eq!(&key[..16], &resources_hash);
881        assert_eq!(&key[16..32], &consumers_hash);
882    }
883
884    #[test]
885    fn test_consumers_key_does_not_length_prefix_account_id() {
886        // The account_id should appear verbatim in the last 32 bytes of the key.
887        let account_id = [0xabu8; 32];
888        let key = consumers_key(&account_id);
889        // Last 32 bytes = raw account_id (no length prefix).
890        assert_eq!(&key[48..], &account_id);
891    }
892
893    // -----------------------------------------------------------------------
894    // decode_username_info_owner
895    // -----------------------------------------------------------------------
896
897    #[test]
898    fn test_decode_username_info_owner_empty_returns_none() {
899        assert_eq!(decode_username_info_owner(&[]), Ok(None));
900    }
901
902    #[test]
903    fn test_decode_username_info_owner_valid_33_bytes() {
904        // 32 bytes owner + 1 byte provider (Authority = 0x00).
905        let owner = [0x42u8; 32];
906        let mut data = Vec::with_capacity(33);
907        data.extend_from_slice(&owner);
908        data.push(0x00); // provider: Authority
909        assert_eq!(decode_username_info_owner(&data), Ok(Some(owner)));
910    }
911
912    #[test]
913    fn test_decode_username_info_owner_ignores_extra_bytes() {
914        // More than 33 bytes is fine — we only extract the first 32.
915        let owner = [0xabu8; 32];
916        let mut data = Vec::with_capacity(40);
917        data.extend_from_slice(&owner);
918        data.extend_from_slice(&[0x01u8; 8]); // extra bytes
919        assert_eq!(decode_username_info_owner(&data), Ok(Some(owner)));
920    }
921
922    #[test]
923    fn test_decode_username_info_owner_truncated_returns_error() {
924        // Only 10 bytes — too short for a 32-byte owner.
925        let data = vec![0x42u8; 10];
926        assert!(matches!(
927            decode_username_info_owner(&data),
928            Err(IdentityError::InvalidResponse { .. })
929        ));
930    }
931
932    #[test]
933    fn test_decode_username_info_owner_exactly_32_bytes() {
934        // Exactly 32 bytes is valid (provider byte missing but we only need owner).
935        let owner = [0x11u8; 32];
936        assert_eq!(decode_username_info_owner(&owner), Ok(Some(owner)));
937    }
938
939    // -----------------------------------------------------------------------
940    // decode_option_account_id
941    // -----------------------------------------------------------------------
942
943    #[test]
944    fn test_decodes_empty_slice_as_none() {
945        assert_eq!(decode_option_account_id(&[]), Ok(None));
946    }
947
948    #[test]
949    fn test_decodes_zero_tag_as_none() {
950        assert_eq!(decode_option_account_id(&[0x00]), Ok(None));
951    }
952
953    #[test]
954    fn test_decodes_some_with_valid_account_id() {
955        let mut data = vec![0x01u8];
956        let account = [0x42u8; 32];
957        data.extend_from_slice(&account);
958        let result = decode_option_account_id(&data).unwrap();
959        assert_eq!(result, Some(account));
960    }
961
962    #[test]
963    fn test_decodes_all_zeros_account_id() {
964        let mut data = vec![0x01u8];
965        data.extend_from_slice(&[0x00u8; 32]);
966        let result = decode_option_account_id(&data).unwrap();
967        assert_eq!(result, Some([0x00u8; 32]));
968    }
969
970    #[test]
971    fn test_decodes_all_ff_account_id() {
972        let mut data = vec![0x01u8];
973        data.extend_from_slice(&[0xffu8; 32]);
974        let result = decode_option_account_id(&data).unwrap();
975        assert_eq!(result, Some([0xffu8; 32]));
976    }
977
978    #[test]
979    fn test_rejects_truncated_some_response() {
980        // Tag = Some but only 10 bytes of payload instead of 32.
981        let mut data = vec![0x01u8];
982        data.extend_from_slice(&[0x00u8; 10]);
983        assert!(matches!(
984            decode_option_account_id(&data),
985            Err(IdentityError::InvalidResponse { .. })
986        ));
987    }
988
989    #[test]
990    fn test_rejects_trailing_bytes_after_none() {
991        // Tag = None but with extra garbage.
992        let data = vec![0x00u8, 0xde, 0xad];
993        assert!(matches!(
994            decode_option_account_id(&data),
995            Err(IdentityError::InvalidResponse { .. })
996        ));
997    }
998
999    #[test]
1000    fn test_rejects_trailing_bytes_after_some() {
1001        // Tag = Some, 32-byte account, then extra byte.
1002        let mut data = vec![0x01u8];
1003        data.extend_from_slice(&[0x42u8; 32]);
1004        data.push(0xff); // trailing garbage
1005        assert!(matches!(
1006            decode_option_account_id(&data),
1007            Err(IdentityError::InvalidResponse { .. })
1008        ));
1009    }
1010
1011    #[test]
1012    fn test_rejects_unknown_tag_byte() {
1013        let data = vec![0x02u8];
1014        assert!(matches!(
1015            decode_option_account_id(&data),
1016            Err(IdentityError::InvalidResponse { .. })
1017        ));
1018    }
1019
1020    #[test]
1021    fn test_rejects_garbage_tag_byte() {
1022        let data = vec![0xffu8, 0x00, 0x00];
1023        assert!(matches!(
1024            decode_option_account_id(&data),
1025            Err(IdentityError::InvalidResponse { .. })
1026        ));
1027    }
1028
1029    // -----------------------------------------------------------------------
1030    // account_id_to_hex
1031    // -----------------------------------------------------------------------
1032
1033    #[test]
1034    fn test_converts_account_id_to_known_hex_string() {
1035        let account = [0xabu8; 32];
1036        let hex = account_id_to_hex(&account);
1037        // 0x prefix + 32 bytes * 2 hex chars each = 66 chars total.
1038        assert_eq!(hex.len(), 66);
1039        assert!(hex.starts_with("0x"));
1040        // Build the expected string programmatically to avoid off-by-one in the literal.
1041        let expected = format!("0x{}", "ab".repeat(32));
1042        assert_eq!(hex, expected);
1043    }
1044
1045    #[test]
1046    fn test_converts_zero_account_id_to_hex() {
1047        let account = [0x00u8; 32];
1048        let hex = account_id_to_hex(&account);
1049        assert_eq!(
1050            hex,
1051            "0x0000000000000000000000000000000000000000000000000000000000000000"
1052        );
1053    }
1054
1055    #[test]
1056    fn test_converts_mixed_account_id_to_lowercase_hex() {
1057        let mut account = [0x00u8; 32];
1058        account[0] = 0xAB;
1059        account[31] = 0xCD;
1060        let hex = account_id_to_hex(&account);
1061        // Must be lowercase.
1062        assert!(hex.chars().all(|c| !c.is_uppercase()));
1063        assert!(hex.starts_with("0xab"));
1064        assert!(hex.ends_with("cd"));
1065    }
1066
1067    // -----------------------------------------------------------------------
1068    // decode_consumer_info
1069    // -----------------------------------------------------------------------
1070
1071    /// Build a minimal valid ConsumerInfo SCALE encoding.
1072    fn build_consumer_info_bytes(
1073        identifier_key: &[u8; 65],
1074        full_username: Option<&[u8]>,
1075        lite_username: &[u8],
1076        credibility_tag: u8,
1077        alias: Option<&[u8; 32]>,
1078        last_update: Option<u64>,
1079    ) -> Vec<u8> {
1080        let mut buf = Vec::new();
1081        buf.extend_from_slice(identifier_key);
1082
1083        // full_username: Option<BoundedVec<u8, 32>>
1084        match full_username {
1085            None => buf.push(0x00),
1086            Some(s) => {
1087                buf.push(0x01);
1088                // SCALE compact length (single-byte mode for len <= 63)
1089                buf.push((s.len() as u8) << 2);
1090                buf.extend_from_slice(s);
1091            }
1092        }
1093
1094        // lite_username: BoundedVec<u8, 32>
1095        buf.push((lite_username.len() as u8) << 2);
1096        buf.extend_from_slice(lite_username);
1097
1098        // credibility
1099        buf.push(credibility_tag);
1100        if credibility_tag == 0x01 {
1101            buf.extend_from_slice(alias.unwrap());
1102            buf.extend_from_slice(&last_update.unwrap().to_le_bytes());
1103        }
1104        buf
1105    }
1106
1107    #[test]
1108    fn test_decode_consumer_info_lite_credibility() {
1109        let identifier_key = [0x04u8; 65];
1110        let lite_username = b"alice.dot";
1111        let data = build_consumer_info_bytes(
1112            &identifier_key,
1113            None,
1114            lite_username,
1115            0x00, // Lite
1116            None,
1117            None,
1118        );
1119
1120        let info = decode_consumer_info(&data).unwrap();
1121        assert_eq!(info.identifier_key, identifier_key);
1122        assert_eq!(info.full_username, None);
1123        assert_eq!(info.lite_username, "alice.dot");
1124        assert_eq!(info.credibility, Credibility::Lite);
1125    }
1126
1127    #[test]
1128    fn test_decode_consumer_info_person_credibility() {
1129        let identifier_key = [0x04u8; 65];
1130        let full_username = b"alice.person";
1131        let lite_username = b"alice";
1132        let alias = [0xbbu8; 32];
1133        let last_update: u64 = 1_700_000_000;
1134
1135        let data = build_consumer_info_bytes(
1136            &identifier_key,
1137            Some(full_username),
1138            lite_username,
1139            0x01, // Person
1140            Some(&alias),
1141            Some(last_update),
1142        );
1143
1144        let info = decode_consumer_info(&data).unwrap();
1145        assert_eq!(info.identifier_key, identifier_key);
1146        assert_eq!(info.full_username, Some("alice.person".to_string()));
1147        assert_eq!(info.lite_username, "alice");
1148        assert_eq!(info.credibility, Credibility::Person { alias, last_update });
1149    }
1150
1151    #[test]
1152    fn test_decode_consumer_info_rejects_empty_data() {
1153        assert!(matches!(
1154            decode_consumer_info(&[]),
1155            Err(IdentityError::InvalidResponse { .. })
1156        ));
1157    }
1158
1159    #[test]
1160    fn test_decode_consumer_info_rejects_truncated_identifier_key() {
1161        // Only 10 bytes — way too short for the 65-byte identifier_key.
1162        let data = vec![0x04u8; 10];
1163        assert!(matches!(
1164            decode_consumer_info(&data),
1165            Err(IdentityError::InvalidResponse { .. })
1166        ));
1167    }
1168
1169    #[test]
1170    fn test_decode_consumer_info_rejects_unknown_credibility_tag() {
1171        let identifier_key = [0x04u8; 65];
1172        let mut data = build_consumer_info_bytes(&identifier_key, None, b"alice", 0x00, None, None);
1173        // Overwrite the credibility byte with an unknown tag.
1174        *data.last_mut().unwrap() = 0x05;
1175        assert!(matches!(
1176            decode_consumer_info(&data),
1177            Err(IdentityError::InvalidResponse { .. })
1178        ));
1179    }
1180
1181    #[test]
1182    fn test_decode_consumer_info_person_truncated_alias() {
1183        let identifier_key = [0x04u8; 65];
1184        let lite_username = b"alice";
1185        let mut data = Vec::new();
1186        data.extend_from_slice(&identifier_key);
1187        data.push(0x00); // full_username: None
1188        data.push((lite_username.len() as u8) << 2);
1189        data.extend_from_slice(lite_username);
1190        data.push(0x01); // Person tag
1191                         // Only 10 bytes of alias instead of 32
1192        data.extend_from_slice(&[0xaau8; 10]);
1193
1194        assert!(matches!(
1195            decode_consumer_info(&data),
1196            Err(IdentityError::InvalidResponse { .. })
1197        ));
1198    }
1199
1200    #[test]
1201    fn test_decode_consumer_info_ignores_trailing_bytes() {
1202        // Fields after credibility (e.g. stmt_store_slots) are silently ignored.
1203        let identifier_key = [0x04u8; 65];
1204        let mut data = build_consumer_info_bytes(&identifier_key, None, b"alice", 0x00, None, None);
1205        data.extend_from_slice(&[0xff, 0xff, 0xff]); // trailing bytes from next field
1206
1207        // Should succeed — extra bytes are intentionally skipped.
1208        assert!(decode_consumer_info(&data).is_ok());
1209    }
1210}