1use thiserror::Error;
11
12pub use crate::{hex_decode, hex_encode};
14
15use crate::dotns::scale_compact_len;
18
19#[derive(Debug, Error, PartialEq)]
21pub enum IdentityError {
22 #[error("username is too long: {len} bytes (max 32)")]
24 UsernameTooLong { len: usize },
25
26 #[error("username must not be empty")]
28 UsernameEmpty,
29
30 #[error("username contains invalid character: {ch:?}")]
32 InvalidCharacter { ch: char },
33
34 #[error("username has invalid dot placement: {reason}")]
36 InvalidDotPlacement { reason: &'static str },
37
38 #[error("invalid response: {msg}")]
40 InvalidResponse { msg: String },
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ConsumerInfo {
50 pub identifier_key: Vec<u8>,
52 pub full_username: Option<String>,
54 pub lite_username: String,
56 pub credibility: Credibility,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum Credibility {
63 Lite,
65 Person {
67 alias: [u8; 32],
69 last_update: u64,
71 },
72}
73
74pub fn normalize_username(username: &str) -> Result<Vec<u8>, IdentityError> {
92 if username.is_empty() {
93 return Err(IdentityError::UsernameEmpty);
94 }
95
96 if username.contains('\0') {
99 return Err(IdentityError::InvalidCharacter { ch: '\0' });
100 }
101
102 let lower = username.to_ascii_lowercase();
105 let bytes = lower.as_bytes();
106
107 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 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
141pub 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 let mut encoded_username = Vec::with_capacity(username_bytes.len() + 4);
159 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 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
176pub 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 let mut encoded_username = Vec::with_capacity(username_bytes.len() + 4);
197 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 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
214pub 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
245pub 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 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
270pub fn decode_option_account_id(data: &[u8]) -> Result<Option<[u8; 32]>, IdentityError> {
280 if data.is_empty() {
282 return Ok(None);
283 }
284
285 match data[0] {
286 0x00 => {
287 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 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
327pub 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
350pub 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 let identifier_key = read_fixed(data, &mut cursor, 65)?;
374
375 let full_username = decode_option_bounded_vec_utf8(data, &mut cursor, "full_username")?;
377
378 let lite_username = decode_bounded_vec_utf8(data, &mut cursor, "lite_username")?;
380
381 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
392pub fn account_id_to_hex(account_id: &[u8; 32]) -> String {
394 hex_encode(account_id)
395}
396
397fn 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
423fn 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 *cursor += 1;
439 Ok((first >> 2) as usize)
440 }
441 0b01 => {
442 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
459fn 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
472fn 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
494fn 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 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
527fn 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
551fn 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#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[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 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 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 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 #[test]
717 fn test_username_info_of_key_length_single_byte_username() {
718 let key = username_info_of_key(b"a");
719 assert_eq!(key.len(), 50);
721 }
722
723 #[test]
724 fn test_username_info_of_key_length_longer_username() {
725 let key = username_info_of_key(b"alice.dot");
727 assert_eq!(key.len(), 16 + 16 + 16 + 1 + 9);
728 }
729
730 #[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 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 #[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 #[test]
761 fn test_username_info_of_key_prefix_pinned() {
762 let key = username_info_of_key(b"a");
763 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 #[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 #[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 #[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 assert_ne!(info_key, owner_key);
816 assert_ne!(&info_key[..32], &owner_key[..32]);
819 }
820
821 #[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 #[test]
834 fn test_decode_username_owner_empty() {
835 assert_eq!(decode_username_owner(&[]), Ok(None));
836 }
837
838 #[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 #[test]
853 fn test_consumers_key_has_correct_length() {
854 let account_id = [0x42u8; 32];
855 let key = consumers_key(&account_id);
856 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 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 let account_id = [0xabu8; 32];
888 let key = consumers_key(&account_id);
889 assert_eq!(&key[48..], &account_id);
891 }
892
893 #[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 let owner = [0x42u8; 32];
906 let mut data = Vec::with_capacity(33);
907 data.extend_from_slice(&owner);
908 data.push(0x00); 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 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]); 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 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 let owner = [0x11u8; 32];
936 assert_eq!(decode_username_info_owner(&owner), Ok(Some(owner)));
937 }
938
939 #[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 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 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 let mut data = vec![0x01u8];
1003 data.extend_from_slice(&[0x42u8; 32]);
1004 data.push(0xff); 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 #[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 assert_eq!(hex.len(), 66);
1039 assert!(hex.starts_with("0x"));
1040 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 assert!(hex.chars().all(|c| !c.is_uppercase()));
1063 assert!(hex.starts_with("0xab"));
1064 assert!(hex.ends_with("cd"));
1065 }
1066
1067 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 match full_username {
1085 None => buf.push(0x00),
1086 Some(s) => {
1087 buf.push(0x01);
1088 buf.push((s.len() as u8) << 2);
1090 buf.extend_from_slice(s);
1091 }
1092 }
1093
1094 buf.push((lite_username.len() as u8) << 2);
1096 buf.extend_from_slice(lite_username);
1097
1098 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, 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, 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 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 *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); data.push((lite_username.len() as u8) << 2);
1189 data.extend_from_slice(lite_username);
1190 data.push(0x01); 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 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]); assert!(decode_consumer_info(&data).is_ok());
1209 }
1210}