use blake2::digest::Mac;
use blake2::{digest::consts::U32, Blake2b, Digest};
type Blake2b256 = Blake2b<U32>;
pub type Topic = [u8; 32];
pub fn string_to_topic(s: &str) -> Topic {
let mut hasher = Blake2b256::new();
hasher.update(s.as_bytes());
let result = hasher.finalize();
let mut topic = [0u8; 32];
topic.copy_from_slice(&result);
topic
}
#[derive(Debug, Clone, PartialEq)]
pub struct Statement {
pub proof_pubkey: Option<[u8; 32]>,
pub proof_signature: Option<[u8; 64]>,
pub decryption_key: Option<Topic>,
pub channel: Option<Topic>,
pub priority: u32,
pub topics: Vec<Topic>,
pub data: Vec<u8>,
}
pub fn encode_compact_u32(val: u32) -> Vec<u8> {
if val < 0x40 {
vec![(val as u8) << 2]
} else if val < 0x4000 {
let v = (val << 2) | 0x01;
vec![v as u8, (v >> 8) as u8]
} else if val < 0x4000_0000 {
let v = (val << 2) | 0x02;
v.to_le_bytes().to_vec()
} else {
let mut out = vec![0x03];
out.extend_from_slice(&val.to_le_bytes());
out
}
}
pub fn decode_compact_u32(data: &[u8]) -> Result<(u32, usize), String> {
if data.is_empty() {
return Err("compact: empty".into());
}
let mode = data[0] & 0x03;
match mode {
0 => Ok(((data[0] >> 2) as u32, 1)),
1 => {
if data.len() < 2 {
return Err("compact: truncated 2-byte".into());
}
let v = u16::from_le_bytes([data[0], data[1]]) >> 2;
Ok((v as u32, 2))
}
2 => {
if data.len() < 4 {
return Err("compact: truncated 4-byte".into());
}
let v = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) >> 2;
Ok((v, 4))
}
3 => {
if data.len() < 5 {
return Err("compact: truncated big".into());
}
let v = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
Ok((v, 5))
}
_ => unreachable!(),
}
}
pub fn build_signing_payload(
now_secs: u64,
decryption_key: Option<&Topic>,
channel: Option<&Topic>,
priority: u32,
topics: &[Topic],
data: &[u8],
) -> Result<(Vec<u8>, u32), String> {
if topics.len() > 4 {
return Err(format!("too many topics ({}, max 4)", topics.len()));
}
let expiry_ts = (now_secs + 3600) as u32;
let expiry: u64 = ((expiry_ts as u64) << 32) | (priority as u64);
let mut num_fields: u32 = 1; if decryption_key.is_some() {
num_fields += 1;
}
num_fields += 1; if channel.is_some() {
num_fields += 1;
}
num_fields += topics.len() as u32;
if !data.is_empty() {
num_fields += 1;
}
let mut payload = Vec::new();
if let Some(dk) = decryption_key {
payload.push(1u8); payload.extend_from_slice(dk);
}
payload.push(2u8); payload.extend_from_slice(&expiry.to_le_bytes());
if let Some(ch) = channel {
payload.push(3u8); payload.extend_from_slice(ch);
}
for (i, t) in topics.iter().enumerate() {
payload.push(4u8 + i as u8); payload.extend_from_slice(t);
}
if !data.is_empty() {
payload.push(8u8); payload.extend_from_slice(&encode_compact_u32(data.len() as u32));
payload.extend_from_slice(data);
}
Ok((payload, num_fields))
}
pub fn assemble_statement(
signing_payload: &[u8],
num_fields: u32,
sr25519_pubkey: &[u8; 32],
signature: &[u8; 64],
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&encode_compact_u32(num_fields));
out.push(0u8); out.push(0u8); out.extend_from_slice(signature);
out.extend_from_slice(sr25519_pubkey);
out.extend_from_slice(signing_payload);
out
}
#[allow(clippy::too_many_arguments)]
pub fn encode_statement(
now_secs: u64,
decryption_key: Option<&Topic>,
channel: Option<&Topic>,
priority: u32,
topics: &[Topic],
data: &[u8],
sr25519_pubkey: &[u8; 32],
sr25519_sign: &dyn Fn(&[u8]) -> [u8; 64],
) -> Result<Vec<u8>, String> {
let (payload, num_fields) =
build_signing_payload(now_secs, decryption_key, channel, priority, topics, data)?;
let signature = sr25519_sign(&payload);
Ok(assemble_statement(
&payload,
num_fields,
sr25519_pubkey,
&signature,
))
}
pub fn extract_signing_payload(encoded: &[u8]) -> Result<&[u8], String> {
if encoded.is_empty() {
return Err("empty statement".into());
}
let (_, compact_len) = decode_compact_u32(encoded)?;
let proof_start = compact_len;
if encoded.len() < proof_start + 2 {
return Err("truncated proof header".into());
}
let tag = encoded[proof_start];
if tag != 0 {
return Err(format!("expected AuthenticityProof tag (0), got {tag}"));
}
let variant = encoded[proof_start + 1];
let proof_body_len = match variant {
0 | 1 => 96, 2 => 98, 3 => 72, _ => return Err(format!("unknown proof variant: {variant}")),
};
let payload_start = proof_start + 2 + proof_body_len;
if encoded.len() < payload_start {
return Err("truncated proof body".into());
}
Ok(&encoded[payload_start..])
}
pub fn decode_statement(encoded: &[u8]) -> Result<Statement, String> {
if encoded.is_empty() {
return Err("empty statement".into());
}
let (num_fields, mut pos) = decode_compact_u32(encoded)?;
let mut proof_pubkey = None;
let mut proof_signature = None;
let mut decryption_key = None;
let mut channel = None;
let mut priority = 0u32;
let mut topics = Vec::new();
let mut data = Vec::new();
for _ in 0..num_fields {
if pos >= encoded.len() {
return Err("truncated field tag".into());
}
let tag = encoded[pos];
pos += 1;
match tag {
0 => {
if pos >= encoded.len() {
return Err("truncated proof variant".into());
}
let variant = encoded[pos];
pos += 1;
match variant {
0 | 1 => {
if pos + 96 > encoded.len() {
return Err("truncated proof".into());
}
let mut sig = [0u8; 64];
sig.copy_from_slice(&encoded[pos..pos + 64]);
proof_signature = Some(sig);
let mut pk = [0u8; 32];
pk.copy_from_slice(&encoded[pos + 64..pos + 96]);
proof_pubkey = Some(pk);
pos += 96;
}
2 => {
if pos + 98 > encoded.len() {
return Err("truncated secp proof".into());
}
pos += 98;
}
3 => {
if pos + 72 > encoded.len() {
return Err("truncated onchain proof".into());
}
let mut pk = [0u8; 32];
pk.copy_from_slice(&encoded[pos..pos + 32]);
proof_pubkey = Some(pk);
pos += 72;
}
_ => return Err(format!("unknown proof variant: {variant}")),
}
}
1 => {
if pos + 32 > encoded.len() {
return Err("truncated decryption_key".into());
}
let mut dk = [0u8; 32];
dk.copy_from_slice(&encoded[pos..pos + 32]);
decryption_key = Some(dk);
pos += 32;
}
2 => {
if pos + 8 > encoded.len() {
return Err("truncated expiry".into());
}
let expiry = u64::from_le_bytes([
encoded[pos],
encoded[pos + 1],
encoded[pos + 2],
encoded[pos + 3],
encoded[pos + 4],
encoded[pos + 5],
encoded[pos + 6],
encoded[pos + 7],
]);
priority = expiry as u32; pos += 8;
}
3 => {
if pos + 32 > encoded.len() {
return Err("truncated channel".into());
}
let mut ch = [0u8; 32];
ch.copy_from_slice(&encoded[pos..pos + 32]);
channel = Some(ch);
pos += 32;
}
4..=7 => {
if pos + 32 > encoded.len() {
return Err("truncated topic".into());
}
let mut t = [0u8; 32];
t.copy_from_slice(&encoded[pos..pos + 32]);
topics.push(t);
pos += 32;
}
8 => {
let (data_len, consumed) =
decode_compact_u32(&encoded[pos..]).map_err(|e| format!("data len: {e}"))?;
pos += consumed;
let data_len = data_len as usize;
if pos + data_len > encoded.len() {
return Err("truncated data".into());
}
data = encoded[pos..pos + data_len].to_vec();
pos += data_len;
}
_ => {
return Err(format!("unknown field tag: {tag}"));
}
}
}
Ok(Statement {
proof_pubkey,
proof_signature,
decryption_key,
channel,
priority,
topics,
data,
})
}
pub fn blake2b_256(data: &[u8]) -> [u8; 32] {
let mut hasher = Blake2b256::new();
hasher.update(data);
let result = hasher.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&result);
out
}
pub fn blake2b_256_keyed(key: &[u8], data: &[u8]) -> Result<[u8; 32], String> {
use blake2::Blake2bMac;
if key.is_empty() {
return Err("blake2b key must not be empty".into());
}
let mut mac = <Blake2bMac<U32> as Mac>::new_from_slice(key)
.map_err(|_| format!("blake2b key length must be 1..=64, got {}", key.len()))?;
mac.update(data);
let result = mac.finalize();
Ok(result.into_bytes().into())
}
pub fn derive_topic_from_account(context: &[u8], account_id: &[u8; 32], extra: &[u8]) -> [u8; 32] {
let ctx_prefix = encode_compact_u32(context.len() as u32);
let mut input = Vec::with_capacity(ctx_prefix.len() + context.len() + 32 + extra.len());
input.extend_from_slice(&ctx_prefix);
input.extend_from_slice(context);
input.extend_from_slice(account_id);
input.extend_from_slice(extra);
blake2b_256(&input)
}
pub use crate::{hex_decode, hex_encode};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_to_topic_deterministic() {
let t1 = string_to_topic("ss-dothost");
let t2 = string_to_topic("ss-dothost");
assert_eq!(t1, t2);
}
#[test]
fn test_string_to_topic_distinct() {
let t1 = string_to_topic("topic-a");
let t2 = string_to_topic("topic-b");
assert_ne!(t1, t2);
}
#[test]
fn test_encode_compact_u32_single_byte() {
assert_eq!(encode_compact_u32(0), vec![0x00]);
assert_eq!(encode_compact_u32(1), vec![0x04]);
assert_eq!(encode_compact_u32(63), vec![0xfc]);
}
#[test]
fn test_decode_compact_u32_empty_returns_error() {
assert!(decode_compact_u32(&[]).is_err());
}
#[test]
fn test_encode_decode_compact_u32_roundtrip() {
for val in [0u32, 1, 63, 64, 16383, 16384, 0x3FFF_FFFF] {
let encoded = encode_compact_u32(val);
let (decoded, _) = decode_compact_u32(&encoded).unwrap();
assert_eq!(decoded, val, "roundtrip failed for {val}");
}
}
#[test]
fn test_build_signing_payload_rejects_too_many_topics() {
let topic = [0u8; 32];
let topics = vec![topic; 5];
let result = build_signing_payload(1_700_000_000, None, None, 0, &topics, b"data");
assert!(result.is_err());
assert!(result.unwrap_err().contains("too many topics"));
}
#[test]
fn test_build_and_assemble_matches_encode_statement() {
let dk = string_to_topic("room-id");
let ch = string_to_topic("channel-1");
let topic = string_to_topic("ss-dothost");
let data = b"hello";
let pubkey = [0xab; 32];
let fake_sig = [0xcd; 64];
let (payload, num_fields) =
build_signing_payload(1_700_000_000, Some(&dk), Some(&ch), 42, &[topic], data).unwrap();
let assembled = assemble_statement(&payload, num_fields, &pubkey, &fake_sig);
let direct = encode_statement(
1_700_000_000,
Some(&dk),
Some(&ch),
42,
&[topic],
data,
&pubkey,
&|_| fake_sig,
)
.unwrap();
assert_eq!(assembled, direct);
}
#[test]
fn test_encode_statement_rejects_too_many_topics() {
let topic = [0u8; 32];
let topics = vec![topic; 5];
let pubkey = [0u8; 32];
let result = encode_statement(
1_700_000_000,
None,
None,
0,
&topics,
b"data",
&pubkey,
&|_| [0u8; 64],
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("too many topics"));
}
#[test]
fn test_encode_decode_statement_roundtrip() {
let decryption_key = string_to_topic("room-id");
let channel = string_to_topic("channel-1");
let topic1 = string_to_topic("ss-dothost");
let topic2 = string_to_topic("presence");
let data = b"hello world";
let pubkey = [0xab; 32];
let fake_sig = [0xcd; 64];
let encoded = encode_statement(
1_700_000_000,
Some(&decryption_key),
Some(&channel),
42,
&[topic1, topic2],
data,
&pubkey,
&|_| fake_sig,
)
.unwrap();
let decoded = decode_statement(&encoded).unwrap();
assert_eq!(decoded.proof_pubkey, Some(pubkey));
assert_eq!(decoded.decryption_key, Some(decryption_key));
assert_eq!(decoded.channel, Some(channel));
assert_eq!(decoded.priority, 42);
assert_eq!(decoded.topics.len(), 2);
assert_eq!(decoded.topics[0], topic1);
assert_eq!(decoded.topics[1], topic2);
assert_eq!(decoded.data, data);
}
#[test]
fn test_decode_statement_empty_returns_error() {
assert!(decode_statement(&[]).is_err());
}
#[test]
fn test_decode_statement_truncated_returns_error() {
assert!(decode_statement(&[0x04, 0x00]).is_err());
}
#[test]
fn test_hex_encode_decode_roundtrip() {
let original = vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
let encoded = hex_encode(&original);
assert_eq!(encoded, "0x0123456789abcdef");
let decoded = hex_decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_hex_decode_bare_string() {
let decoded = hex_decode("deadbeef").unwrap();
assert_eq!(decoded, vec![0xde, 0xad, 0xbe, 0xef]);
}
#[test]
fn test_hex_decode_rejects_odd_length() {
assert!(hex_decode("0xabc").is_none());
}
#[test]
fn test_blake2b_256_deterministic() {
let h1 = blake2b_256(b"test");
let h2 = blake2b_256(b"test");
assert_eq!(h1, h2);
assert_ne!(h1, [0u8; 32]);
}
#[test]
fn test_encode_compact_u32_two_byte_mode() {
let encoded = encode_compact_u32(64);
assert_eq!(encoded.len(), 2);
let (decoded, _) = decode_compact_u32(&encoded).unwrap();
assert_eq!(decoded, 64);
}
#[test]
fn test_encode_compact_u32_four_byte_mode() {
let encoded = encode_compact_u32(16384);
assert_eq!(encoded.len(), 4);
let (decoded, _) = decode_compact_u32(&encoded).unwrap();
assert_eq!(decoded, 16384);
}
#[test]
fn test_encode_compact_u32_big_mode() {
let val = 0x4000_0000u32;
let encoded = encode_compact_u32(val);
assert_eq!(encoded.len(), 5);
assert_eq!(encoded[0], 0x03); let (decoded, _) = decode_compact_u32(&encoded).unwrap();
assert_eq!(decoded, val);
}
#[test]
fn test_decode_compact_u32_two_byte_truncated_returns_error() {
assert!(decode_compact_u32(&[0x01]).is_err());
}
#[test]
fn test_decode_compact_u32_four_byte_truncated_returns_error() {
assert!(decode_compact_u32(&[0x02, 0x00, 0x00]).is_err());
}
#[test]
fn test_decode_compact_u32_big_mode_truncated_returns_error() {
assert!(decode_compact_u32(&[0x03, 0x00, 0x00, 0x00]).is_err());
}
fn minimal_statement_bytes(proof_variant: u8, proof_bytes: &[u8], data: &[u8]) -> Vec<u8> {
let num_fields: u32 = if data.is_empty() { 1 } else { 2 };
let mut out = encode_compact_u32(num_fields);
out.push(0u8); out.push(proof_variant);
out.extend_from_slice(proof_bytes);
if !data.is_empty() {
out.push(8u8); out.extend_from_slice(&encode_compact_u32(data.len() as u32));
out.extend_from_slice(data);
}
out
}
#[test]
fn test_decode_statement_ed25519_proof_variant() {
let sig = [0x11u8; 64];
let pk = [0x22u8; 32];
let mut proof_bytes = Vec::new();
proof_bytes.extend_from_slice(&sig);
proof_bytes.extend_from_slice(&pk);
let encoded = minimal_statement_bytes(1, &proof_bytes, b"");
let decoded = decode_statement(&encoded).unwrap();
assert_eq!(decoded.proof_pubkey, Some(pk));
}
#[test]
fn test_decode_statement_secp256k1_proof_variant() {
let proof_bytes = vec![0x33u8; 98];
let encoded = minimal_statement_bytes(2, &proof_bytes, b"");
let decoded = decode_statement(&encoded).unwrap();
assert_eq!(decoded.proof_pubkey, None);
}
#[test]
fn test_decode_statement_onchain_proof_variant() {
let who = [0x44u8; 32];
let block_hash = [0x55u8; 32];
let block_num = [0x00u8; 8];
let mut proof_bytes = Vec::new();
proof_bytes.extend_from_slice(&who);
proof_bytes.extend_from_slice(&block_hash);
proof_bytes.extend_from_slice(&block_num);
let encoded = minimal_statement_bytes(3, &proof_bytes, b"");
let decoded = decode_statement(&encoded).unwrap();
assert_eq!(decoded.proof_pubkey, Some(who));
}
#[test]
fn test_decode_statement_unknown_proof_variant_returns_error() {
let encoded = minimal_statement_bytes(99, &[0u8; 10], b"");
assert!(decode_statement(&encoded).is_err());
}
#[test]
fn test_decode_statement_unknown_field_tag_returns_error() {
let mut out = encode_compact_u32(1);
out.push(9u8); assert!(decode_statement(&out).is_err());
}
#[test]
fn test_decode_statement_truncated_proof_returns_error() {
let encoded = minimal_statement_bytes(0, &[0u8; 10], b"");
assert!(decode_statement(&encoded).is_err());
}
#[test]
fn test_decode_statement_truncated_secp_proof_returns_error() {
let encoded = minimal_statement_bytes(2, &[0u8; 10], b"");
assert!(decode_statement(&encoded).is_err());
}
#[test]
fn test_decode_statement_truncated_onchain_proof_returns_error() {
let encoded = minimal_statement_bytes(3, &[0u8; 10], b"");
assert!(decode_statement(&encoded).is_err());
}
#[test]
fn test_decode_statement_truncated_decryption_key_returns_error() {
let mut out = encode_compact_u32(2);
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());
}
#[test]
fn test_decode_statement_truncated_channel_returns_error() {
let mut out = encode_compact_u32(2);
out.push(0u8); out.push(0u8); out.extend_from_slice(&[0u8; 96]);
out.push(3u8); out.extend_from_slice(&[0u8; 10]); assert!(decode_statement(&out).is_err());
}
#[test]
fn test_decode_statement_truncated_topic_returns_error() {
let mut out = encode_compact_u32(2);
out.push(0u8); out.push(0u8); out.extend_from_slice(&[0u8; 96]);
out.push(4u8); out.extend_from_slice(&[0u8; 10]); assert!(decode_statement(&out).is_err());
}
#[test]
fn test_decode_statement_truncated_data_returns_error() {
let mut out = encode_compact_u32(2);
out.push(0u8); out.push(0u8); out.extend_from_slice(&[0u8; 96]);
out.push(8u8); out.extend_from_slice(&encode_compact_u32(50));
out.extend_from_slice(&[0u8; 5]);
assert!(decode_statement(&out).is_err());
}
#[test]
fn test_decode_statement_truncated_expiry_returns_error() {
let mut out = encode_compact_u32(2);
out.push(0u8); out.push(0u8); out.extend_from_slice(&[0u8; 96]);
out.push(2u8); out.extend_from_slice(&[0u8; 4]); assert!(decode_statement(&out).is_err());
}
#[test]
fn test_encode_statement_minimal_no_optional_fields() {
let pubkey = [0xabu8; 32];
let fake_sig = [0xcdu8; 64];
let encoded = encode_statement(
1_700_000_000,
None, None, 0,
&[], &[], &pubkey,
&|_| fake_sig,
)
.unwrap();
let decoded = decode_statement(&encoded).unwrap();
assert_eq!(decoded.proof_pubkey, Some(pubkey));
assert_eq!(decoded.decryption_key, None);
assert_eq!(decoded.channel, None);
assert_eq!(decoded.topics.len(), 0);
assert_eq!(decoded.data, b"");
}
#[test]
fn test_encode_statement_expiry_encodes_priority_in_lower_bits() {
let pubkey = [0u8; 32];
let fake_sig = [0u8; 64];
let priority = 0x0000_cafe_u32;
let encoded = encode_statement(
1_700_000_000,
None,
None,
priority,
&[],
&[],
&pubkey,
&|_| fake_sig,
)
.unwrap();
let decoded = decode_statement(&encoded).unwrap();
assert_eq!(decoded.priority, priority);
}
#[test]
fn test_encode_statement_max_topics_succeeds() {
let topic = [0u8; 32];
let topics = vec![topic; 4]; let pubkey = [0u8; 32];
let result = encode_statement(1_700_000_000, None, None, 0, &topics, b"", &pubkey, &|_| {
[0u8; 64]
});
assert!(result.is_ok());
let decoded = decode_statement(&result.unwrap()).unwrap();
assert_eq!(decoded.topics.len(), 4);
}
#[test]
fn test_blake2b_256_keyed_empty_key_returns_error() {
let result = blake2b_256_keyed(b"", b"data");
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("blake2b key must not be empty"));
}
#[test]
fn test_blake2b_256_keyed_64_byte_key_succeeds() {
let key = [0xabu8; 64];
let result = blake2b_256_keyed(&key, b"data");
assert!(result.is_ok());
assert_ne!(result.unwrap(), [0u8; 32]);
}
#[test]
fn test_blake2b_256_keyed_65_byte_key_returns_error() {
let key = [0xabu8; 65];
let result = blake2b_256_keyed(&key, b"data");
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("blake2b key length must be 1..=64, got 65"));
}
#[test]
fn test_blake2b_256_keyed_pinned_vector() {
let key = [0x01u8; 32];
let data = b"polkadot";
let digest = blake2b_256_keyed(&key, data).unwrap();
let expected: [u8; 32] = [
0xdc, 0xbc, 0x39, 0xc6, 0x21, 0xe8, 0xc2, 0x0c, 0x84, 0xc1, 0x81, 0x6b, 0x18, 0x3d,
0x7c, 0xae, 0x76, 0x11, 0x7b, 0x36, 0x16, 0x0c, 0xd3, 0x3f, 0xda, 0x54, 0x8f, 0x91,
0x14, 0x49, 0x98, 0x05,
];
assert_eq!(
digest, expected,
"keyed blake2b digest must match pinned vector"
);
}
#[test]
fn test_derive_topic_from_account_domain_separation() {
let account_id = [0x42u8; 32];
let t1 = derive_topic_from_account(b"ab", &account_id, b"c");
let t2 = derive_topic_from_account(b"a", &account_id, b"bc");
assert_ne!(
t1, t2,
"length-prefixed context must prevent domain collision"
);
}
#[test]
fn test_derive_topic_from_account_deterministic() {
let account_id = [0x01u8; 32];
let t1 = derive_topic_from_account(b"ctx", &account_id, b"extra");
let t2 = derive_topic_from_account(b"ctx", &account_id, b"extra");
assert_eq!(t1, t2);
assert_ne!(t1, [0u8; 32]);
}
#[test]
fn test_derive_topic_from_account_empty_extra() {
let account_id = [0x77u8; 32];
let result = derive_topic_from_account(b"context", &account_id, b"");
assert_ne!(result, [0u8; 32]);
}
#[test]
fn test_derive_topic_from_account_uses_scale_compact_prefix() {
let context = b"chat-request";
let account_id = [0x01u8; 32];
let extra = 0u64.to_le_bytes();
let mut expected_input = Vec::new();
expected_input.push(0x30); expected_input.extend_from_slice(context);
expected_input.extend_from_slice(&account_id);
expected_input.extend_from_slice(&extra);
let expected = blake2b_256(&expected_input);
let actual = derive_topic_from_account(context, &account_id, &extra);
assert_eq!(
actual, expected,
"derive_topic_from_account must use SCALE compact prefix (iOS compatibility)"
);
}
#[test]
fn test_derive_topic_from_account_empty_context() {
let account_id = [0x01u8; 32];
let mut expected_input = Vec::new();
expected_input.push(0x00); expected_input.extend_from_slice(&account_id);
let expected = blake2b_256(&expected_input);
let actual = derive_topic_from_account(b"", &account_id, b"");
assert_eq!(actual, expected);
}
#[test]
fn test_derive_topic_from_account_two_byte_compact_context() {
let context = vec![b'x'; 64];
let account_id = [0x02u8; 32];
let mut expected_input = Vec::new();
expected_input.extend_from_slice(&encode_compact_u32(64));
expected_input.extend_from_slice(&context);
expected_input.extend_from_slice(&account_id);
let expected = blake2b_256(&expected_input);
let actual = derive_topic_from_account(&context, &account_id, b"");
assert_eq!(actual, expected);
}
}