Skip to main content

host_encoding/
statement_store.rs

1//! Pure sp-statement-store encoding and decoding functions.
2//!
3//! No I/O. No network calls. WASM-safe.
4//!
5//! Implements the binary statement format used by Substrate's statement store pallet.
6//! Callers are responsible for providing the current time (as Unix seconds) so that
7//! this module remains free of `std::time` and is WASM-safe.
8
9use blake2::digest::Mac;
10use blake2::{digest::consts::U32, Blake2b, Digest};
11
12type Blake2b256 = Blake2b<U32>;
13
14/// Topic = 32-byte blake2b hash.
15pub type Topic = [u8; 32];
16
17/// Hash a string into a 32-byte Topic (blake2b-256).
18pub 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// ---------------------------------------------------------------------------
28// Statement binary encoding (sp-statement-store compatible)
29//
30// Substrate format: Compact<u32> field count, then tagged fields in order.
31// Field tags: 0=Proof, 1=DecryptionKey, 2=Expiry, 3=Channel, 4-7=Topics, 8=Data
32// Each field: u8 tag + SCALE-encoded payload.
33// Proof::Sr25519 = tag 0, variant 0, sig[64], signer[32].
34// Data = tag 8, Compact<u32> length, bytes.
35// Signing payload: same fields without Compact prefix and without Proof field.
36// ---------------------------------------------------------------------------
37
38/// A decoded statement from the statement store.
39#[derive(Debug, Clone, PartialEq)]
40pub struct Statement {
41    pub proof_pubkey: Option<[u8; 32]>,
42    /// Raw sr25519 (or ed25519) signature over the signing payload.
43    /// Present only for proof variants 0 (Sr25519) and 1 (Ed25519).
44    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
52/// Encode SCALE Compact<u32>.
53pub 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
69/// Decode SCALE Compact<u32>, returns (value, bytes_consumed).
70pub 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
102/// Build the signing payload for a statement.
103///
104/// Returns `(payload_bytes, num_fields)` where `payload_bytes` are the raw bytes
105/// that must be signed (sr25519 context signing or equivalent), and `num_fields`
106/// is needed by [`assemble_statement`] to write the SCALE header.
107///
108/// # Parameters
109///
110/// - `now_secs`: current Unix timestamp in seconds. Used to compute a 1-hour expiry.
111/// - `topics`: at most 4 topics; returns `Err` if more are supplied.
112pub 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    // Expiry: upper 32 bits = timestamp (seconds), lower 32 bits = priority.
125    let expiry_ts = (now_secs + 3600) as u32;
126    let expiry: u64 = ((expiry_ts as u64) << 32) | (priority as u64);
127
128    // Count fields (proof + data fields).
129    let mut num_fields: u32 = 1; // proof always present
130    if decryption_key.is_some() {
131        num_fields += 1;
132    }
133    num_fields += 1; // expiry always present
134    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    // Build signing payload (fields without Compact prefix and without Proof).
143    let mut payload = Vec::new();
144    if let Some(dk) = decryption_key {
145        payload.push(1u8); // tag: DecryptionKey
146        payload.extend_from_slice(dk);
147    }
148    payload.push(2u8); // tag: Expiry
149    payload.extend_from_slice(&expiry.to_le_bytes());
150    if let Some(ch) = channel {
151        payload.push(3u8); // tag: Channel
152        payload.extend_from_slice(ch);
153    }
154    for (i, t) in topics.iter().enumerate() {
155        payload.push(4u8 + i as u8); // tag: Topic1..4
156        payload.extend_from_slice(t);
157    }
158    if !data.is_empty() {
159        payload.push(8u8); // tag: Data
160        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
167/// Assemble a complete encoded statement from a signing payload and sr25519 signature.
168///
169/// `signing_payload` and `num_fields` must come from [`build_signing_payload`].
170pub 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    // Compact<u32> field count prefix.
179    out.extend_from_slice(&encode_compact_u32(num_fields));
180
181    // Field 0: AuthenticityProof (Proof::Sr25519 = variant 0)
182    out.push(0u8); // tag: AuthenticityProof
183    out.push(0u8); // Proof variant 0 = Sr25519
184    out.extend_from_slice(signature);
185    out.extend_from_slice(sr25519_pubkey);
186
187    // Remaining fields (same as signing_payload).
188    out.extend_from_slice(signing_payload);
189
190    out
191}
192
193/// Encode a statement into the sp-statement-store SCALE binary format.
194///
195/// Convenience wrapper that calls [`build_signing_payload`] then signs and
196/// assembles in one step. For FFI or two-phase workflows where the signing
197/// closure is unavailable, use the split API instead.
198///
199/// # Parameters
200///
201/// - `now_secs`: current Unix timestamp in seconds. Used to compute a 1-hour
202///   expiry. Callers must supply this to keep the function pure and WASM-safe.
203/// - `topics`: at most 4 topics; returns `Err` if more are supplied.
204// All 8 parameters are required by the sp-statement-store wire format;
205// grouping them into a struct would add churn for callers with no semantic gain.
206#[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
228/// Extract the signing payload from a raw encoded statement.
229///
230/// The signing payload is the portion of the statement that was signed: all
231/// fields except the Compact field-count prefix and the AuthenticityProof field
232/// itself.  Callers use this to verify an embedded signature before trusting
233/// any other field.
234///
235/// Returns `Err` if the statement is too short or malformed.
236pub fn extract_signing_payload(encoded: &[u8]) -> Result<&[u8], String> {
237    if encoded.is_empty() {
238        return Err("empty statement".into());
239    }
240    // Skip the Compact<u32> field-count prefix.
241    let (_, compact_len) = decode_compact_u32(encoded)?;
242
243    // The proof field must be first: tag(1B) + variant(1B) + sig[64] + pk[32] = 98 B
244    // for variants 0 (Sr25519) and 1 (Ed25519).  We only handle those here because
245    // the chat protocol uses Sr25519 exclusively.
246    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, // sig[64] + pubkey[32]
257        2 => 98,     // secp256k1: sig[65] + signer[33]
258        3 => 72,     // on-chain: who[32] + block_hash[32] + u64
259        _ => 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
268/// Decode a statement from SCALE binary encoding (sp-statement-store format).
269pub 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                // AuthenticityProof
294                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                        // Sr25519 or Ed25519: sig[64] + signer[32]
302                        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                        // Secp256k1: sig[65] + signer[33]
315                        if pos + 98 > encoded.len() {
316                            return Err("truncated secp proof".into());
317                        }
318                        pos += 98;
319                    }
320                    3 => {
321                        // OnChain: who[32] + block_hash[32] + u64
322                        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                // DecryptionKey [32]
335                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                // Expiry u64 — upper 32 = timestamp, lower 32 = priority
345                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; // lower 32 bits
359                pos += 8;
360            }
361            3 => {
362                // Channel [32]
363                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                // Topic [32]
373                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                // Data: Compact<u32> length + bytes
383                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                // Unknown field — can't decode further without knowing size.
395                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
411/// Compute a blake2b-256 hash.
412pub 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
421/// Keyed Blake2b-256 hash.
422///
423/// `key` must be 1..=64 bytes (Blake2b key length constraint).
424/// Returns the 32-byte digest, or an error if the key length is invalid.
425pub fn blake2b_256_keyed(key: &[u8], data: &[u8]) -> Result<[u8; 32], String> {
426    use blake2::Blake2bMac;
427    // Reject empty keys explicitly — some Blake2b implementations silently
428    // degrade to unkeyed mode for key.len()==0, which would be a security bug.
429    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
439/// Derive a 32-byte topic from a context string, a raw AccountId, and optional extra bytes.
440///
441/// `topic = blake2b_256(SCALE_compact(context.len()) || context || account_id || extra)`
442///
443/// The context is length-prefixed with SCALE compact encoding to match the
444/// deployed iOS protocol (`Data.encode(scaleEncoder:)` produces a SCALE
445/// `Vec<u8>` prefix). AccountId is always exactly 32 bytes (enforced by
446/// `&[u8; 32]`), so it acts as a fixed-width separator and needs no length
447/// prefix.
448pub 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
458// Re-export shared hex utilities from the crate root.
459pub 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    // -----------------------------------------------------------------------
629    // encode_compact_u32 — two-byte and four-byte mode coverage
630    // -----------------------------------------------------------------------
631
632    #[test]
633    fn test_encode_compact_u32_two_byte_mode() {
634        // 64 <= val < 16384 → two-byte mode, lower 2 bits = 0b01
635        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        // 16384 <= val < 0x40000000 → four-byte mode
644        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        // val >= 0x40000000 → big mode (5 bytes: 0x03 prefix + 4-byte value)
653        let val = 0x4000_0000u32;
654        let encoded = encode_compact_u32(val);
655        assert_eq!(encoded.len(), 5);
656        assert_eq!(encoded[0], 0x03); // big-mode prefix
657        let (decoded, _) = decode_compact_u32(&encoded).unwrap();
658        assert_eq!(decoded, val);
659    }
660
661    // -----------------------------------------------------------------------
662    // decode_compact_u32 — truncation error paths
663    // -----------------------------------------------------------------------
664
665    #[test]
666    fn test_decode_compact_u32_two_byte_truncated_returns_error() {
667        // Mode bit = 0b01 but only one byte
668        assert!(decode_compact_u32(&[0x01]).is_err());
669    }
670
671    #[test]
672    fn test_decode_compact_u32_four_byte_truncated_returns_error() {
673        // Mode bit = 0b10 but fewer than 4 bytes
674        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        // Mode bit = 0b11 (big mode) but fewer than 5 bytes
680        assert!(decode_compact_u32(&[0x03, 0x00, 0x00, 0x00]).is_err());
681    }
682
683    // -----------------------------------------------------------------------
684    // decode_statement — proof variant coverage
685    // -----------------------------------------------------------------------
686
687    /// Helper that builds a signed statement with one data field using a dummy key.
688    fn minimal_statement_bytes(proof_variant: u8, proof_bytes: &[u8], data: &[u8]) -> Vec<u8> {
689        // Two fields: proof + data (if non-empty), or just proof if data is empty.
690        let num_fields: u32 = if data.is_empty() { 1 } else { 2 };
691        let mut out = encode_compact_u32(num_fields);
692        out.push(0u8); // tag: AuthenticityProof
693        out.push(proof_variant);
694        out.extend_from_slice(proof_bytes);
695        if !data.is_empty() {
696            out.push(8u8); // tag: Data
697            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        // Variant 1 = Ed25519: sig[64] + signer[32] = 96 bytes, same layout as Sr25519
706        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        // Variant 2 = Secp256k1: sig[65] + signer[33] = 98 bytes
720        let proof_bytes = vec![0x33u8; 98];
721        let encoded = minimal_statement_bytes(2, &proof_bytes, b"");
722        let decoded = decode_statement(&encoded).unwrap();
723        // Secp256k1 proof does not set proof_pubkey in the current implementation
724        assert_eq!(decoded.proof_pubkey, None);
725    }
726
727    #[test]
728    fn test_decode_statement_onchain_proof_variant() {
729        // Variant 3 = OnChain: who[32] + block_hash[32] + u64[8] = 72 bytes
730        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        // Variant 99 is not recognized
746        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        // Field count = 1, then use tag 9 (unknown after 8)
753        let mut out = encode_compact_u32(1);
754        out.push(9u8); // unknown tag
755        assert!(decode_statement(&out).is_err());
756    }
757
758    #[test]
759    fn test_decode_statement_truncated_proof_returns_error() {
760        // Proof variant 0 needs 96 bytes but only provide 10
761        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        // Variant 2 needs 98 bytes but only provide 10
768        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        // Variant 3 needs 72 bytes but only provide 10
775        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        // 2 fields: proof (valid) + decryption_key (truncated)
782        let mut out = encode_compact_u32(2);
783        out.push(0u8); // AuthenticityProof
784        out.push(0u8); // Sr25519
785        out.extend_from_slice(&[0u8; 96]); // valid proof
786        out.push(1u8); // DecryptionKey tag
787        out.extend_from_slice(&[0u8; 10]); // only 10 bytes, needs 32
788        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); // AuthenticityProof
795        out.push(0u8); // Sr25519
796        out.extend_from_slice(&[0u8; 96]);
797        out.push(3u8); // Channel tag
798        out.extend_from_slice(&[0u8; 10]); // only 10 bytes, needs 32
799        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); // AuthenticityProof
806        out.push(0u8); // Sr25519
807        out.extend_from_slice(&[0u8; 96]);
808        out.push(4u8); // Topic1 tag
809        out.extend_from_slice(&[0u8; 10]); // only 10 bytes, needs 32
810        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); // AuthenticityProof
817        out.push(0u8); // Sr25519
818        out.extend_from_slice(&[0u8; 96]);
819        out.push(8u8); // Data tag
820                       // compact_len claims 50 bytes but we only provide 5
821        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); // AuthenticityProof
830        out.push(0u8); // Sr25519
831        out.extend_from_slice(&[0u8; 96]);
832        out.push(2u8); // Expiry tag
833        out.extend_from_slice(&[0u8; 4]); // only 4 bytes, needs 8
834        assert!(decode_statement(&out).is_err());
835    }
836
837    // -----------------------------------------------------------------------
838    // encode_statement — minimal case (no optional fields, no data)
839    // -----------------------------------------------------------------------
840
841    #[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, // no decryption key
848            None, // no channel
849            0,
850            &[], // no topics
851            &[], // no data
852            &pubkey,
853            &|_| fake_sig,
854        )
855        .unwrap();
856
857        // Should decode successfully and have no optional fields
858        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        // The priority is stored in the lower 32 bits of the expiry u64
886        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]; // exactly 4 — maximum allowed
893        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    // -----------------------------------------------------------------------
903    // blake2b_256_keyed
904    // -----------------------------------------------------------------------
905
906    #[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        // Non-zero digest confirms something was computed.
921        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    /// Pinned test vector: blake2b_256_keyed(key=[0x01;32], data=b"polkadot").
935    ///
936    /// The expected value was computed by running `blake2b_256_keyed` and
937    /// recording the output; it detects any future regression in the digest.
938    #[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        // Computed once and pinned for regression detection.
944        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    // -----------------------------------------------------------------------
956    // derive_topic_from_account
957    // -----------------------------------------------------------------------
958
959    /// Verifies that redistributing bytes between `context` and `extra`
960    /// changes the output — the length prefix prevents concatenation collisions.
961    #[test]
962    fn test_derive_topic_from_account_domain_separation() {
963        let account_id = [0x42u8; 32];
964        // "ab" as context + "c" as extra  vs  "a" as context + "bc" as extra.
965        // Without the length prefix these would produce identical inputs.
966        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    /// Verify that derive_topic_from_account uses SCALE compact encoding for the
991    /// context length prefix, matching the deployed iOS protocol.
992    ///
993    /// For context = "chat-request" (12 bytes), SCALE compact encodes 12 as a
994    /// single byte: 12 * 4 = 48 = 0x30.
995    #[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        // Build the expected preimage manually with SCALE compact prefix.
1002        let mut expected_input = Vec::new();
1003        expected_input.push(0x30); // SCALE compact(12) = single byte
1004        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); // SCALE compact(0)
1021        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        // Length 64 triggers 2-byte SCALE compact: (64<<2)|1 = 257 = [0x01, 0x01]
1030        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}