1use blake2::digest::Mac;
10use blake2::{digest::consts::U32, Blake2b, Digest};
11
12type Blake2b256 = Blake2b<U32>;
13
14pub type Topic = [u8; 32];
16
17pub fn string_to_topic(s: &str) -> Topic {
19 let mut hasher = Blake2b256::new();
20 hasher.update(s.as_bytes());
21 let result = hasher.finalize();
22 let mut topic = [0u8; 32];
23 topic.copy_from_slice(&result);
24 topic
25}
26
27#[derive(Debug, Clone, PartialEq)]
40pub struct Statement {
41 pub proof_pubkey: Option<[u8; 32]>,
42 pub proof_signature: Option<[u8; 64]>,
45 pub decryption_key: Option<Topic>,
46 pub channel: Option<Topic>,
47 pub priority: u32,
48 pub topics: Vec<Topic>,
49 pub data: Vec<u8>,
50}
51
52pub fn encode_compact_u32(val: u32) -> Vec<u8> {
54 if val < 0x40 {
55 vec![(val as u8) << 2]
56 } else if val < 0x4000 {
57 let v = (val << 2) | 0x01;
58 vec![v as u8, (v >> 8) as u8]
59 } else if val < 0x4000_0000 {
60 let v = (val << 2) | 0x02;
61 v.to_le_bytes().to_vec()
62 } else {
63 let mut out = vec![0x03];
64 out.extend_from_slice(&val.to_le_bytes());
65 out
66 }
67}
68
69pub fn decode_compact_u32(data: &[u8]) -> Result<(u32, usize), String> {
71 if data.is_empty() {
72 return Err("compact: empty".into());
73 }
74 let mode = data[0] & 0x03;
75 match mode {
76 0 => Ok(((data[0] >> 2) as u32, 1)),
77 1 => {
78 if data.len() < 2 {
79 return Err("compact: truncated 2-byte".into());
80 }
81 let v = u16::from_le_bytes([data[0], data[1]]) >> 2;
82 Ok((v as u32, 2))
83 }
84 2 => {
85 if data.len() < 4 {
86 return Err("compact: truncated 4-byte".into());
87 }
88 let v = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) >> 2;
89 Ok((v, 4))
90 }
91 3 => {
92 if data.len() < 5 {
93 return Err("compact: truncated big".into());
94 }
95 let v = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
96 Ok((v, 5))
97 }
98 _ => unreachable!(),
99 }
100}
101
102pub fn build_signing_payload(
113 now_secs: u64,
114 decryption_key: Option<&Topic>,
115 channel: Option<&Topic>,
116 priority: u32,
117 topics: &[Topic],
118 data: &[u8],
119) -> Result<(Vec<u8>, u32), String> {
120 if topics.len() > 4 {
121 return Err(format!("too many topics ({}, max 4)", topics.len()));
122 }
123
124 let expiry_ts = (now_secs + 3600) as u32;
126 let expiry: u64 = ((expiry_ts as u64) << 32) | (priority as u64);
127
128 let mut num_fields: u32 = 1; if decryption_key.is_some() {
131 num_fields += 1;
132 }
133 num_fields += 1; if channel.is_some() {
135 num_fields += 1;
136 }
137 num_fields += topics.len() as u32;
138 if !data.is_empty() {
139 num_fields += 1;
140 }
141
142 let mut payload = Vec::new();
144 if let Some(dk) = decryption_key {
145 payload.push(1u8); payload.extend_from_slice(dk);
147 }
148 payload.push(2u8); payload.extend_from_slice(&expiry.to_le_bytes());
150 if let Some(ch) = channel {
151 payload.push(3u8); payload.extend_from_slice(ch);
153 }
154 for (i, t) in topics.iter().enumerate() {
155 payload.push(4u8 + i as u8); payload.extend_from_slice(t);
157 }
158 if !data.is_empty() {
159 payload.push(8u8); payload.extend_from_slice(&encode_compact_u32(data.len() as u32));
161 payload.extend_from_slice(data);
162 }
163
164 Ok((payload, num_fields))
165}
166
167pub fn assemble_statement(
171 signing_payload: &[u8],
172 num_fields: u32,
173 sr25519_pubkey: &[u8; 32],
174 signature: &[u8; 64],
175) -> Vec<u8> {
176 let mut out = Vec::new();
177
178 out.extend_from_slice(&encode_compact_u32(num_fields));
180
181 out.push(0u8); out.push(0u8); out.extend_from_slice(signature);
185 out.extend_from_slice(sr25519_pubkey);
186
187 out.extend_from_slice(signing_payload);
189
190 out
191}
192
193#[allow(clippy::too_many_arguments)]
207pub fn encode_statement(
208 now_secs: u64,
209 decryption_key: Option<&Topic>,
210 channel: Option<&Topic>,
211 priority: u32,
212 topics: &[Topic],
213 data: &[u8],
214 sr25519_pubkey: &[u8; 32],
215 sr25519_sign: &dyn Fn(&[u8]) -> [u8; 64],
216) -> Result<Vec<u8>, String> {
217 let (payload, num_fields) =
218 build_signing_payload(now_secs, decryption_key, channel, priority, topics, data)?;
219 let signature = sr25519_sign(&payload);
220 Ok(assemble_statement(
221 &payload,
222 num_fields,
223 sr25519_pubkey,
224 &signature,
225 ))
226}
227
228pub fn extract_signing_payload(encoded: &[u8]) -> Result<&[u8], String> {
237 if encoded.is_empty() {
238 return Err("empty statement".into());
239 }
240 let (_, compact_len) = decode_compact_u32(encoded)?;
242
243 let proof_start = compact_len;
247 if encoded.len() < proof_start + 2 {
248 return Err("truncated proof header".into());
249 }
250 let tag = encoded[proof_start];
251 if tag != 0 {
252 return Err(format!("expected AuthenticityProof tag (0), got {tag}"));
253 }
254 let variant = encoded[proof_start + 1];
255 let proof_body_len = match variant {
256 0 | 1 => 96, 2 => 98, 3 => 72, _ => return Err(format!("unknown proof variant: {variant}")),
260 };
261 let payload_start = proof_start + 2 + proof_body_len;
262 if encoded.len() < payload_start {
263 return Err("truncated proof body".into());
264 }
265 Ok(&encoded[payload_start..])
266}
267
268pub fn decode_statement(encoded: &[u8]) -> Result<Statement, String> {
270 if encoded.is_empty() {
271 return Err("empty statement".into());
272 }
273
274 let (num_fields, mut pos) = decode_compact_u32(encoded)?;
275
276 let mut proof_pubkey = None;
277 let mut proof_signature = None;
278 let mut decryption_key = None;
279 let mut channel = None;
280 let mut priority = 0u32;
281 let mut topics = Vec::new();
282 let mut data = Vec::new();
283
284 for _ in 0..num_fields {
285 if pos >= encoded.len() {
286 return Err("truncated field tag".into());
287 }
288 let tag = encoded[pos];
289 pos += 1;
290
291 match tag {
292 0 => {
293 if pos >= encoded.len() {
295 return Err("truncated proof variant".into());
296 }
297 let variant = encoded[pos];
298 pos += 1;
299 match variant {
300 0 | 1 => {
301 if pos + 96 > encoded.len() {
303 return Err("truncated proof".into());
304 }
305 let mut sig = [0u8; 64];
306 sig.copy_from_slice(&encoded[pos..pos + 64]);
307 proof_signature = Some(sig);
308 let mut pk = [0u8; 32];
309 pk.copy_from_slice(&encoded[pos + 64..pos + 96]);
310 proof_pubkey = Some(pk);
311 pos += 96;
312 }
313 2 => {
314 if pos + 98 > encoded.len() {
316 return Err("truncated secp proof".into());
317 }
318 pos += 98;
319 }
320 3 => {
321 if pos + 72 > encoded.len() {
323 return Err("truncated onchain proof".into());
324 }
325 let mut pk = [0u8; 32];
326 pk.copy_from_slice(&encoded[pos..pos + 32]);
327 proof_pubkey = Some(pk);
328 pos += 72;
329 }
330 _ => return Err(format!("unknown proof variant: {variant}")),
331 }
332 }
333 1 => {
334 if pos + 32 > encoded.len() {
336 return Err("truncated decryption_key".into());
337 }
338 let mut dk = [0u8; 32];
339 dk.copy_from_slice(&encoded[pos..pos + 32]);
340 decryption_key = Some(dk);
341 pos += 32;
342 }
343 2 => {
344 if pos + 8 > encoded.len() {
346 return Err("truncated expiry".into());
347 }
348 let expiry = u64::from_le_bytes([
349 encoded[pos],
350 encoded[pos + 1],
351 encoded[pos + 2],
352 encoded[pos + 3],
353 encoded[pos + 4],
354 encoded[pos + 5],
355 encoded[pos + 6],
356 encoded[pos + 7],
357 ]);
358 priority = expiry as u32; pos += 8;
360 }
361 3 => {
362 if pos + 32 > encoded.len() {
364 return Err("truncated channel".into());
365 }
366 let mut ch = [0u8; 32];
367 ch.copy_from_slice(&encoded[pos..pos + 32]);
368 channel = Some(ch);
369 pos += 32;
370 }
371 4..=7 => {
372 if pos + 32 > encoded.len() {
374 return Err("truncated topic".into());
375 }
376 let mut t = [0u8; 32];
377 t.copy_from_slice(&encoded[pos..pos + 32]);
378 topics.push(t);
379 pos += 32;
380 }
381 8 => {
382 let (data_len, consumed) =
384 decode_compact_u32(&encoded[pos..]).map_err(|e| format!("data len: {e}"))?;
385 pos += consumed;
386 let data_len = data_len as usize;
387 if pos + data_len > encoded.len() {
388 return Err("truncated data".into());
389 }
390 data = encoded[pos..pos + data_len].to_vec();
391 pos += data_len;
392 }
393 _ => {
394 return Err(format!("unknown field tag: {tag}"));
396 }
397 }
398 }
399
400 Ok(Statement {
401 proof_pubkey,
402 proof_signature,
403 decryption_key,
404 channel,
405 priority,
406 topics,
407 data,
408 })
409}
410
411pub fn blake2b_256(data: &[u8]) -> [u8; 32] {
413 let mut hasher = Blake2b256::new();
414 hasher.update(data);
415 let result = hasher.finalize();
416 let mut out = [0u8; 32];
417 out.copy_from_slice(&result);
418 out
419}
420
421pub fn blake2b_256_keyed(key: &[u8], data: &[u8]) -> Result<[u8; 32], String> {
426 use blake2::Blake2bMac;
427 if key.is_empty() {
430 return Err("blake2b key must not be empty".into());
431 }
432 let mut mac = <Blake2bMac<U32> as Mac>::new_from_slice(key)
433 .map_err(|_| format!("blake2b key length must be 1..=64, got {}", key.len()))?;
434 mac.update(data);
435 let result = mac.finalize();
436 Ok(result.into_bytes().into())
437}
438
439pub fn derive_topic_from_account(context: &[u8], account_id: &[u8; 32], extra: &[u8]) -> [u8; 32] {
449 let ctx_prefix = encode_compact_u32(context.len() as u32);
450 let mut input = Vec::with_capacity(ctx_prefix.len() + context.len() + 32 + extra.len());
451 input.extend_from_slice(&ctx_prefix);
452 input.extend_from_slice(context);
453 input.extend_from_slice(account_id);
454 input.extend_from_slice(extra);
455 blake2b_256(&input)
456}
457
458pub use crate::{hex_decode, hex_encode};
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_string_to_topic_deterministic() {
467 let t1 = string_to_topic("ss-dothost");
468 let t2 = string_to_topic("ss-dothost");
469 assert_eq!(t1, t2);
470 }
471
472 #[test]
473 fn test_string_to_topic_distinct() {
474 let t1 = string_to_topic("topic-a");
475 let t2 = string_to_topic("topic-b");
476 assert_ne!(t1, t2);
477 }
478
479 #[test]
480 fn test_encode_compact_u32_single_byte() {
481 assert_eq!(encode_compact_u32(0), vec![0x00]);
482 assert_eq!(encode_compact_u32(1), vec![0x04]);
483 assert_eq!(encode_compact_u32(63), vec![0xfc]);
484 }
485
486 #[test]
487 fn test_decode_compact_u32_empty_returns_error() {
488 assert!(decode_compact_u32(&[]).is_err());
489 }
490
491 #[test]
492 fn test_encode_decode_compact_u32_roundtrip() {
493 for val in [0u32, 1, 63, 64, 16383, 16384, 0x3FFF_FFFF] {
494 let encoded = encode_compact_u32(val);
495 let (decoded, _) = decode_compact_u32(&encoded).unwrap();
496 assert_eq!(decoded, val, "roundtrip failed for {val}");
497 }
498 }
499
500 #[test]
501 fn test_build_signing_payload_rejects_too_many_topics() {
502 let topic = [0u8; 32];
503 let topics = vec![topic; 5];
504 let result = build_signing_payload(1_700_000_000, None, None, 0, &topics, b"data");
505 assert!(result.is_err());
506 assert!(result.unwrap_err().contains("too many topics"));
507 }
508
509 #[test]
510 fn test_build_and_assemble_matches_encode_statement() {
511 let dk = string_to_topic("room-id");
512 let ch = string_to_topic("channel-1");
513 let topic = string_to_topic("ss-dothost");
514 let data = b"hello";
515 let pubkey = [0xab; 32];
516 let fake_sig = [0xcd; 64];
517
518 let (payload, num_fields) =
519 build_signing_payload(1_700_000_000, Some(&dk), Some(&ch), 42, &[topic], data).unwrap();
520
521 let assembled = assemble_statement(&payload, num_fields, &pubkey, &fake_sig);
522 let direct = encode_statement(
523 1_700_000_000,
524 Some(&dk),
525 Some(&ch),
526 42,
527 &[topic],
528 data,
529 &pubkey,
530 &|_| fake_sig,
531 )
532 .unwrap();
533
534 assert_eq!(assembled, direct);
535 }
536
537 #[test]
538 fn test_encode_statement_rejects_too_many_topics() {
539 let topic = [0u8; 32];
540 let topics = vec![topic; 5];
541 let pubkey = [0u8; 32];
542 let result = encode_statement(
543 1_700_000_000,
544 None,
545 None,
546 0,
547 &topics,
548 b"data",
549 &pubkey,
550 &|_| [0u8; 64],
551 );
552 assert!(result.is_err());
553 assert!(result.unwrap_err().contains("too many topics"));
554 }
555
556 #[test]
557 fn test_encode_decode_statement_roundtrip() {
558 let decryption_key = string_to_topic("room-id");
559 let channel = string_to_topic("channel-1");
560 let topic1 = string_to_topic("ss-dothost");
561 let topic2 = string_to_topic("presence");
562 let data = b"hello world";
563 let pubkey = [0xab; 32];
564 let fake_sig = [0xcd; 64];
565
566 let encoded = encode_statement(
567 1_700_000_000,
568 Some(&decryption_key),
569 Some(&channel),
570 42,
571 &[topic1, topic2],
572 data,
573 &pubkey,
574 &|_| fake_sig,
575 )
576 .unwrap();
577
578 let decoded = decode_statement(&encoded).unwrap();
579
580 assert_eq!(decoded.proof_pubkey, Some(pubkey));
581 assert_eq!(decoded.decryption_key, Some(decryption_key));
582 assert_eq!(decoded.channel, Some(channel));
583 assert_eq!(decoded.priority, 42);
584 assert_eq!(decoded.topics.len(), 2);
585 assert_eq!(decoded.topics[0], topic1);
586 assert_eq!(decoded.topics[1], topic2);
587 assert_eq!(decoded.data, data);
588 }
589
590 #[test]
591 fn test_decode_statement_empty_returns_error() {
592 assert!(decode_statement(&[]).is_err());
593 }
594
595 #[test]
596 fn test_decode_statement_truncated_returns_error() {
597 assert!(decode_statement(&[0x04, 0x00]).is_err());
598 }
599
600 #[test]
601 fn test_hex_encode_decode_roundtrip() {
602 let original = vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
603 let encoded = hex_encode(&original);
604 assert_eq!(encoded, "0x0123456789abcdef");
605 let decoded = hex_decode(&encoded).unwrap();
606 assert_eq!(decoded, original);
607 }
608
609 #[test]
610 fn test_hex_decode_bare_string() {
611 let decoded = hex_decode("deadbeef").unwrap();
612 assert_eq!(decoded, vec![0xde, 0xad, 0xbe, 0xef]);
613 }
614
615 #[test]
616 fn test_hex_decode_rejects_odd_length() {
617 assert!(hex_decode("0xabc").is_none());
618 }
619
620 #[test]
621 fn test_blake2b_256_deterministic() {
622 let h1 = blake2b_256(b"test");
623 let h2 = blake2b_256(b"test");
624 assert_eq!(h1, h2);
625 assert_ne!(h1, [0u8; 32]);
626 }
627
628 #[test]
633 fn test_encode_compact_u32_two_byte_mode() {
634 let encoded = encode_compact_u32(64);
636 assert_eq!(encoded.len(), 2);
637 let (decoded, _) = decode_compact_u32(&encoded).unwrap();
638 assert_eq!(decoded, 64);
639 }
640
641 #[test]
642 fn test_encode_compact_u32_four_byte_mode() {
643 let encoded = encode_compact_u32(16384);
645 assert_eq!(encoded.len(), 4);
646 let (decoded, _) = decode_compact_u32(&encoded).unwrap();
647 assert_eq!(decoded, 16384);
648 }
649
650 #[test]
651 fn test_encode_compact_u32_big_mode() {
652 let val = 0x4000_0000u32;
654 let encoded = encode_compact_u32(val);
655 assert_eq!(encoded.len(), 5);
656 assert_eq!(encoded[0], 0x03); let (decoded, _) = decode_compact_u32(&encoded).unwrap();
658 assert_eq!(decoded, val);
659 }
660
661 #[test]
666 fn test_decode_compact_u32_two_byte_truncated_returns_error() {
667 assert!(decode_compact_u32(&[0x01]).is_err());
669 }
670
671 #[test]
672 fn test_decode_compact_u32_four_byte_truncated_returns_error() {
673 assert!(decode_compact_u32(&[0x02, 0x00, 0x00]).is_err());
675 }
676
677 #[test]
678 fn test_decode_compact_u32_big_mode_truncated_returns_error() {
679 assert!(decode_compact_u32(&[0x03, 0x00, 0x00, 0x00]).is_err());
681 }
682
683 fn minimal_statement_bytes(proof_variant: u8, proof_bytes: &[u8], data: &[u8]) -> Vec<u8> {
689 let num_fields: u32 = if data.is_empty() { 1 } else { 2 };
691 let mut out = encode_compact_u32(num_fields);
692 out.push(0u8); out.push(proof_variant);
694 out.extend_from_slice(proof_bytes);
695 if !data.is_empty() {
696 out.push(8u8); out.extend_from_slice(&encode_compact_u32(data.len() as u32));
698 out.extend_from_slice(data);
699 }
700 out
701 }
702
703 #[test]
704 fn test_decode_statement_ed25519_proof_variant() {
705 let sig = [0x11u8; 64];
707 let pk = [0x22u8; 32];
708 let mut proof_bytes = Vec::new();
709 proof_bytes.extend_from_slice(&sig);
710 proof_bytes.extend_from_slice(&pk);
711
712 let encoded = minimal_statement_bytes(1, &proof_bytes, b"");
713 let decoded = decode_statement(&encoded).unwrap();
714 assert_eq!(decoded.proof_pubkey, Some(pk));
715 }
716
717 #[test]
718 fn test_decode_statement_secp256k1_proof_variant() {
719 let proof_bytes = vec![0x33u8; 98];
721 let encoded = minimal_statement_bytes(2, &proof_bytes, b"");
722 let decoded = decode_statement(&encoded).unwrap();
723 assert_eq!(decoded.proof_pubkey, None);
725 }
726
727 #[test]
728 fn test_decode_statement_onchain_proof_variant() {
729 let who = [0x44u8; 32];
731 let block_hash = [0x55u8; 32];
732 let block_num = [0x00u8; 8];
733 let mut proof_bytes = Vec::new();
734 proof_bytes.extend_from_slice(&who);
735 proof_bytes.extend_from_slice(&block_hash);
736 proof_bytes.extend_from_slice(&block_num);
737
738 let encoded = minimal_statement_bytes(3, &proof_bytes, b"");
739 let decoded = decode_statement(&encoded).unwrap();
740 assert_eq!(decoded.proof_pubkey, Some(who));
741 }
742
743 #[test]
744 fn test_decode_statement_unknown_proof_variant_returns_error() {
745 let encoded = minimal_statement_bytes(99, &[0u8; 10], b"");
747 assert!(decode_statement(&encoded).is_err());
748 }
749
750 #[test]
751 fn test_decode_statement_unknown_field_tag_returns_error() {
752 let mut out = encode_compact_u32(1);
754 out.push(9u8); assert!(decode_statement(&out).is_err());
756 }
757
758 #[test]
759 fn test_decode_statement_truncated_proof_returns_error() {
760 let encoded = minimal_statement_bytes(0, &[0u8; 10], b"");
762 assert!(decode_statement(&encoded).is_err());
763 }
764
765 #[test]
766 fn test_decode_statement_truncated_secp_proof_returns_error() {
767 let encoded = minimal_statement_bytes(2, &[0u8; 10], b"");
769 assert!(decode_statement(&encoded).is_err());
770 }
771
772 #[test]
773 fn test_decode_statement_truncated_onchain_proof_returns_error() {
774 let encoded = minimal_statement_bytes(3, &[0u8; 10], b"");
776 assert!(decode_statement(&encoded).is_err());
777 }
778
779 #[test]
780 fn test_decode_statement_truncated_decryption_key_returns_error() {
781 let mut out = encode_compact_u32(2);
783 out.push(0u8); out.push(0u8); out.extend_from_slice(&[0u8; 96]); out.push(1u8); out.extend_from_slice(&[0u8; 10]); assert!(decode_statement(&out).is_err());
789 }
790
791 #[test]
792 fn test_decode_statement_truncated_channel_returns_error() {
793 let mut out = encode_compact_u32(2);
794 out.push(0u8); out.push(0u8); out.extend_from_slice(&[0u8; 96]);
797 out.push(3u8); out.extend_from_slice(&[0u8; 10]); assert!(decode_statement(&out).is_err());
800 }
801
802 #[test]
803 fn test_decode_statement_truncated_topic_returns_error() {
804 let mut out = encode_compact_u32(2);
805 out.push(0u8); out.push(0u8); out.extend_from_slice(&[0u8; 96]);
808 out.push(4u8); out.extend_from_slice(&[0u8; 10]); assert!(decode_statement(&out).is_err());
811 }
812
813 #[test]
814 fn test_decode_statement_truncated_data_returns_error() {
815 let mut out = encode_compact_u32(2);
816 out.push(0u8); out.push(0u8); out.extend_from_slice(&[0u8; 96]);
819 out.push(8u8); out.extend_from_slice(&encode_compact_u32(50));
822 out.extend_from_slice(&[0u8; 5]);
823 assert!(decode_statement(&out).is_err());
824 }
825
826 #[test]
827 fn test_decode_statement_truncated_expiry_returns_error() {
828 let mut out = encode_compact_u32(2);
829 out.push(0u8); out.push(0u8); out.extend_from_slice(&[0u8; 96]);
832 out.push(2u8); out.extend_from_slice(&[0u8; 4]); assert!(decode_statement(&out).is_err());
835 }
836
837 #[test]
842 fn test_encode_statement_minimal_no_optional_fields() {
843 let pubkey = [0xabu8; 32];
844 let fake_sig = [0xcdu8; 64];
845 let encoded = encode_statement(
846 1_700_000_000,
847 None, None, 0,
850 &[], &[], &pubkey,
853 &|_| fake_sig,
854 )
855 .unwrap();
856
857 let decoded = decode_statement(&encoded).unwrap();
859 assert_eq!(decoded.proof_pubkey, Some(pubkey));
860 assert_eq!(decoded.decryption_key, None);
861 assert_eq!(decoded.channel, None);
862 assert_eq!(decoded.topics.len(), 0);
863 assert_eq!(decoded.data, b"");
864 }
865
866 #[test]
867 fn test_encode_statement_expiry_encodes_priority_in_lower_bits() {
868 let pubkey = [0u8; 32];
869 let fake_sig = [0u8; 64];
870 let priority = 0x0000_cafe_u32;
871
872 let encoded = encode_statement(
873 1_700_000_000,
874 None,
875 None,
876 priority,
877 &[],
878 &[],
879 &pubkey,
880 &|_| fake_sig,
881 )
882 .unwrap();
883
884 let decoded = decode_statement(&encoded).unwrap();
885 assert_eq!(decoded.priority, priority);
887 }
888
889 #[test]
890 fn test_encode_statement_max_topics_succeeds() {
891 let topic = [0u8; 32];
892 let topics = vec![topic; 4]; let pubkey = [0u8; 32];
894 let result = encode_statement(1_700_000_000, None, None, 0, &topics, b"", &pubkey, &|_| {
895 [0u8; 64]
896 });
897 assert!(result.is_ok());
898 let decoded = decode_statement(&result.unwrap()).unwrap();
899 assert_eq!(decoded.topics.len(), 4);
900 }
901
902 #[test]
907 fn test_blake2b_256_keyed_empty_key_returns_error() {
908 let result = blake2b_256_keyed(b"", b"data");
909 assert!(result.is_err());
910 assert!(result
911 .unwrap_err()
912 .contains("blake2b key must not be empty"));
913 }
914
915 #[test]
916 fn test_blake2b_256_keyed_64_byte_key_succeeds() {
917 let key = [0xabu8; 64];
918 let result = blake2b_256_keyed(&key, b"data");
919 assert!(result.is_ok());
920 assert_ne!(result.unwrap(), [0u8; 32]);
922 }
923
924 #[test]
925 fn test_blake2b_256_keyed_65_byte_key_returns_error() {
926 let key = [0xabu8; 65];
927 let result = blake2b_256_keyed(&key, b"data");
928 assert!(result.is_err());
929 assert!(result
930 .unwrap_err()
931 .contains("blake2b key length must be 1..=64, got 65"));
932 }
933
934 #[test]
939 fn test_blake2b_256_keyed_pinned_vector() {
940 let key = [0x01u8; 32];
941 let data = b"polkadot";
942 let digest = blake2b_256_keyed(&key, data).unwrap();
943 let expected: [u8; 32] = [
945 0xdc, 0xbc, 0x39, 0xc6, 0x21, 0xe8, 0xc2, 0x0c, 0x84, 0xc1, 0x81, 0x6b, 0x18, 0x3d,
946 0x7c, 0xae, 0x76, 0x11, 0x7b, 0x36, 0x16, 0x0c, 0xd3, 0x3f, 0xda, 0x54, 0x8f, 0x91,
947 0x14, 0x49, 0x98, 0x05,
948 ];
949 assert_eq!(
950 digest, expected,
951 "keyed blake2b digest must match pinned vector"
952 );
953 }
954
955 #[test]
962 fn test_derive_topic_from_account_domain_separation() {
963 let account_id = [0x42u8; 32];
964 let t1 = derive_topic_from_account(b"ab", &account_id, b"c");
967 let t2 = derive_topic_from_account(b"a", &account_id, b"bc");
968 assert_ne!(
969 t1, t2,
970 "length-prefixed context must prevent domain collision"
971 );
972 }
973
974 #[test]
975 fn test_derive_topic_from_account_deterministic() {
976 let account_id = [0x01u8; 32];
977 let t1 = derive_topic_from_account(b"ctx", &account_id, b"extra");
978 let t2 = derive_topic_from_account(b"ctx", &account_id, b"extra");
979 assert_eq!(t1, t2);
980 assert_ne!(t1, [0u8; 32]);
981 }
982
983 #[test]
984 fn test_derive_topic_from_account_empty_extra() {
985 let account_id = [0x77u8; 32];
986 let result = derive_topic_from_account(b"context", &account_id, b"");
987 assert_ne!(result, [0u8; 32]);
988 }
989
990 #[test]
996 fn test_derive_topic_from_account_uses_scale_compact_prefix() {
997 let context = b"chat-request";
998 let account_id = [0x01u8; 32];
999 let extra = 0u64.to_le_bytes();
1000
1001 let mut expected_input = Vec::new();
1003 expected_input.push(0x30); expected_input.extend_from_slice(context);
1005 expected_input.extend_from_slice(&account_id);
1006 expected_input.extend_from_slice(&extra);
1007
1008 let expected = blake2b_256(&expected_input);
1009 let actual = derive_topic_from_account(context, &account_id, &extra);
1010 assert_eq!(
1011 actual, expected,
1012 "derive_topic_from_account must use SCALE compact prefix (iOS compatibility)"
1013 );
1014 }
1015
1016 #[test]
1017 fn test_derive_topic_from_account_empty_context() {
1018 let account_id = [0x01u8; 32];
1019 let mut expected_input = Vec::new();
1020 expected_input.push(0x00); expected_input.extend_from_slice(&account_id);
1022 let expected = blake2b_256(&expected_input);
1023 let actual = derive_topic_from_account(b"", &account_id, b"");
1024 assert_eq!(actual, expected);
1025 }
1026
1027 #[test]
1028 fn test_derive_topic_from_account_two_byte_compact_context() {
1029 let context = vec![b'x'; 64];
1031 let account_id = [0x02u8; 32];
1032 let mut expected_input = Vec::new();
1033 expected_input.extend_from_slice(&encode_compact_u32(64));
1034 expected_input.extend_from_slice(&context);
1035 expected_input.extend_from_slice(&account_id);
1036 let expected = blake2b_256(&expected_input);
1037 let actual = derive_topic_from_account(&context, &account_id, b"");
1038 assert_eq!(actual, expected);
1039 }
1040}