use alloc::vec::Vec;
pub const MAX_TOPICS: usize = 4;
pub const MAX_ANY_TOPICS: usize = 128;
const MAX_STATEMENTS_PER_NOTIFICATION: usize = 10_000;
const FIELD_PROOF: u8 = 0;
const FIELD_DECRYPTION_KEY: u8 = 1;
const FIELD_EXPIRY: u8 = 2;
const FIELD_CHANNEL: u8 = 3;
const FIELD_TOPIC_START: u8 = 4;
const FIELD_TOPIC_END: u8 = FIELD_TOPIC_START + MAX_TOPICS as u8 - 1;
const FIELD_DATA: u8 = 8;
const PROOF_SR25519: u8 = 0;
const PROOF_ED25519: u8 = 1;
const PROOF_SECP256K1_ECDSA: u8 = 2;
const PROOF_ON_CHAIN: u8 = 3;
pub use super::affinity::AffinityFilter;
#[derive(Debug, Clone)]
pub enum StatementMessage {
Statements(Vec<([u8; 32], Statement)>),
ExplicitTopicAffinity(AffinityFilter),
}
pub type Topic = [u8; 32];
pub type DecryptionKey = [u8; 32];
pub type Channel = [u8; 32];
pub type AccountId = [u8; 32];
pub type BlockHash = [u8; 32];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Statement {
pub proof: Option<Proof>,
pub decryption_key: Option<DecryptionKey>,
pub expiry: u64,
pub channel: Option<Channel>,
pub topics: Vec<Topic>,
pub data: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Proof {
Sr25519 {
signature: [u8; 64],
signer: [u8; 32],
},
Ed25519 {
signature: [u8; 64],
signer: [u8; 32],
},
Secp256k1Ecdsa {
signature: [u8; 65],
signer: [u8; 33],
},
OnChain {
who: AccountId,
block_hash: BlockHash,
event_index: u64,
},
}
pub fn statement_hash(statement_bytes: &[u8]) -> [u8; 32] {
<[u8; 32]>::try_from(blake2_rfc::blake2b::blake2b(32, &[], statement_bytes).as_bytes())
.expect("blake2b output is 32 bytes; qed")
}
pub fn decode_statement(bytes: &[u8]) -> Result<Statement, DecodeStatementNotificationError> {
match nom::Parser::parse(
&mut nom::combinator::all_consuming::<_, nom::error::Error<&[u8]>, _>(
nom::combinator::complete(statement_parser),
),
bytes,
) {
Ok((_, s)) => Ok(s),
Err(nom::Err::Error(e) | nom::Err::Failure(e)) => {
Err(DecodeStatementNotificationError(e.code))
}
Err(nom::Err::Incomplete(_)) => {
Err(DecodeStatementNotificationError(nom::error::ErrorKind::Eof))
}
}
}
pub fn decode_statement_notification(
scale_encoded: &[u8],
) -> Result<Vec<([u8; 32], Statement)>, DecodeStatementNotificationError> {
let (mut remaining, count) = crate::util::nom_scale_compact_usize(scale_encoded).map_err(
|_: nom::Err<nom::error::Error<&[u8]>>| {
DecodeStatementNotificationError(nom::error::ErrorKind::Fail)
},
)?;
if count > MAX_STATEMENTS_PER_NOTIFICATION {
return Err(DecodeStatementNotificationError(
nom::error::ErrorKind::TooLarge,
));
}
let mut statements = Vec::with_capacity(count);
for _ in 0..count {
let start = remaining;
let (rest, statement) = statement_parser(remaining).map_err(|e| match e {
nom::Err::Error(e) | nom::Err::Failure(e) => DecodeStatementNotificationError(e.code),
nom::Err::Incomplete(_) => DecodeStatementNotificationError(nom::error::ErrorKind::Eof),
})?;
let raw = &start[..start.len() - rest.len()];
let hash = statement_hash(raw);
statements.push((hash, statement));
remaining = rest;
}
if !remaining.is_empty() {
return Err(DecodeStatementNotificationError(
nom::error::ErrorKind::NonEmpty,
));
}
Ok(statements)
}
#[derive(Debug, derive_more::Display, derive_more::Error, Clone)]
#[display("Failed to decode statement notification {_0:?}")]
pub struct DecodeStatementNotificationError(#[error(not(source))] nom::error::ErrorKind);
#[derive(Debug, derive_more::Display, derive_more::Error, Clone)]
pub enum EncodeStatementError {
#[display("Too many topics: got {got}, max {max}")]
TooManyTopics {
got: usize,
max: usize,
},
}
const V2_TAG_STATEMENTS: u8 = 0x00;
const V2_TAG_AFFINITY: u8 = 0x01;
pub fn decode_statement_message(
bytes: &[u8],
) -> Result<StatementMessage, DecodeStatementMessageError> {
if bytes.is_empty() {
return Err(DecodeStatementMessageError::Empty);
}
match bytes[0] {
V2_TAG_STATEMENTS => {
let stmts = decode_statement_notification(&bytes[1..])
.map_err(DecodeStatementMessageError::InvalidStatements)?;
Ok(StatementMessage::Statements(stmts))
}
V2_TAG_AFFINITY => {
let filter = AffinityFilter::decode(&bytes[1..])
.map_err(|_| DecodeStatementMessageError::InvalidBloomFilter)?;
Ok(StatementMessage::ExplicitTopicAffinity(filter))
}
other => Err(DecodeStatementMessageError::UnknownVariant(other)),
}
}
pub fn encode_statements_message(statements: &[&[u8]]) -> Vec<u8> {
let tag_len = 1;
let max_compact_len = 5;
let total_len: usize = statements.iter().map(|s| s.len()).sum();
let mut out = Vec::with_capacity(tag_len + max_compact_len + total_len);
out.push(V2_TAG_STATEMENTS);
out.extend_from_slice(crate::util::encode_scale_compact_usize(statements.len()).as_ref());
for stmt in statements {
out.extend_from_slice(stmt);
}
out
}
pub fn encode_topic_affinity_message(filter: &AffinityFilter) -> Vec<u8> {
let tag_len = 1;
let encoded = filter.encode_to_vec();
let mut out = Vec::with_capacity(tag_len + encoded.len());
out.push(V2_TAG_AFFINITY);
out.extend_from_slice(&encoded);
out
}
#[derive(Debug, derive_more::Display, derive_more::Error, Clone)]
pub enum DecodeStatementMessageError {
#[display("Empty V2 statement message")]
Empty,
#[display("Unknown V2 statement message variant: {_0}")]
UnknownVariant(#[error(not(source))] u8),
#[display("Invalid bloom filter in affinity message")]
InvalidBloomFilter,
#[display("Invalid statements in V2 message: {_0}")]
InvalidStatements(DecodeStatementNotificationError),
}
pub fn encode_statement(statement: &Statement) -> Result<Vec<u8>, EncodeStatementError> {
let mut out = Vec::new();
encode_statement_into(statement, &mut out)?;
Ok(out)
}
fn encode_statement_into(
statement: &Statement,
out: &mut Vec<u8>,
) -> Result<(), EncodeStatementError> {
if statement.topics.len() > MAX_TOPICS {
return Err(EncodeStatementError::TooManyTopics {
got: statement.topics.len(),
max: MAX_TOPICS,
});
}
let num_fields = statement.proof.is_some() as usize
+ statement.decryption_key.is_some() as usize
+ 1 + statement.channel.is_some() as usize
+ statement.topics.len()
+ statement.data.is_some() as usize;
out.extend_from_slice(crate::util::encode_scale_compact_usize(num_fields).as_ref());
if let Some(proof) = &statement.proof {
out.push(FIELD_PROOF);
encode_proof_into(proof, out);
}
if let Some(key) = &statement.decryption_key {
out.push(FIELD_DECRYPTION_KEY);
out.extend_from_slice(key);
}
out.push(FIELD_EXPIRY);
out.extend_from_slice(&statement.expiry.to_le_bytes());
if let Some(channel) = &statement.channel {
out.push(FIELD_CHANNEL);
out.extend_from_slice(channel);
}
for (i, topic) in statement.topics.iter().enumerate() {
out.push(FIELD_TOPIC_START + i as u8);
out.extend_from_slice(topic);
}
if let Some(data) = &statement.data {
out.push(FIELD_DATA);
out.extend_from_slice(crate::util::encode_scale_compact_usize(data.len()).as_ref());
out.extend_from_slice(data);
}
Ok(())
}
fn encode_proof_into(proof: &Proof, out: &mut Vec<u8>) {
match proof {
Proof::Sr25519 { signature, signer } => {
out.push(PROOF_SR25519);
out.extend_from_slice(signature.as_slice());
out.extend_from_slice(signer.as_slice());
}
Proof::Ed25519 { signature, signer } => {
out.push(PROOF_ED25519);
out.extend_from_slice(signature.as_slice());
out.extend_from_slice(signer.as_slice());
}
Proof::Secp256k1Ecdsa { signature, signer } => {
out.push(PROOF_SECP256K1_ECDSA);
out.extend_from_slice(signature.as_slice());
out.extend_from_slice(signer.as_slice());
}
Proof::OnChain {
who,
block_hash,
event_index,
} => {
out.push(PROOF_ON_CHAIN);
out.extend_from_slice(who.as_slice());
out.extend_from_slice(block_hash.as_slice());
out.extend_from_slice(&event_index.to_le_bytes());
}
}
}
fn statement_parser(input: &[u8]) -> nom::IResult<&[u8], Statement> {
let (input, num_fields) = crate::util::nom_scale_compact_usize(input)?;
fields_parser(num_fields)(input)
}
fn fields_parser(num_fields: usize) -> impl FnMut(&[u8]) -> nom::IResult<&[u8], Statement> {
move |mut input: &[u8]| {
let mut proof = None;
let mut decryption_key = None;
let mut expiry = None;
let mut channel = None;
let mut topics = Vec::new();
let mut data = None;
let mut last_tag: Option<u8> = None;
for _ in 0..num_fields {
let (rest, tag) = nom::number::streaming::u8(input)?;
if let Some(lt) = last_tag {
if tag <= lt {
return Err(nom::Err::Failure(nom::error::make_error(
input,
nom::error::ErrorKind::Verify,
)));
}
}
last_tag = Some(tag);
let rest = match tag {
FIELD_PROOF => {
let (rest, p) = proof_parser(rest)?;
proof = Some(p);
rest
}
FIELD_DECRYPTION_KEY => {
let (rest, key) = nom::bytes::streaming::take(32u32)(rest)?;
decryption_key =
Some(<[u8; 32]>::try_from(key).expect("take(32) guarantees 32 bytes; qed"));
rest
}
FIELD_EXPIRY => {
let (rest, exp) = nom::number::streaming::le_u64(rest)?;
expiry = Some(exp);
rest
}
FIELD_CHANNEL => {
let (rest, ch) = nom::bytes::streaming::take(32u32)(rest)?;
channel =
Some(<[u8; 32]>::try_from(ch).expect("take(32) guarantees 32 bytes; qed"));
rest
}
FIELD_TOPIC_START..=FIELD_TOPIC_END => {
let topic_index = (tag - FIELD_TOPIC_START) as usize;
if topic_index != topics.len() {
return Err(nom::Err::Failure(nom::error::make_error(
input,
nom::error::ErrorKind::Verify,
)));
}
let (rest, topic) = nom::bytes::streaming::take(32u32)(rest)?;
topics.push(
<[u8; 32]>::try_from(topic).expect("take(32) guarantees 32 bytes; qed"),
);
rest
}
FIELD_DATA => {
let (rest, len) = crate::util::nom_scale_compact_usize(rest)?;
let (rest, d) = nom::bytes::streaming::take(len)(rest)?;
data = Some(d.to_vec());
rest
}
_ => {
return Err(nom::Err::Failure(nom::error::make_error(
input,
nom::error::ErrorKind::Verify,
)));
}
};
input = rest;
}
let expiry = expiry.ok_or_else(|| {
nom::Err::Failure(nom::error::make_error(input, nom::error::ErrorKind::Verify))
})?;
let statement = Statement {
proof,
decryption_key,
expiry,
channel,
topics,
data,
};
Ok((input, statement))
}
}
fn proof_parser(input: &[u8]) -> nom::IResult<&[u8], Proof> {
let (input, variant) = nom::number::streaming::u8(input)?;
match variant {
PROOF_SR25519 => {
let (input, signature) = nom::bytes::streaming::take(64u32)(input)?;
let (input, signer) = nom::bytes::streaming::take(32u32)(input)?;
Ok((
input,
Proof::Sr25519 {
signature: <[u8; 64]>::try_from(signature)
.expect("take(64) guarantees 64 bytes; qed"),
signer: <[u8; 32]>::try_from(signer)
.expect("take(32) guarantees 32 bytes; qed"),
},
))
}
PROOF_ED25519 => {
let (input, signature) = nom::bytes::streaming::take(64u32)(input)?;
let (input, signer) = nom::bytes::streaming::take(32u32)(input)?;
Ok((
input,
Proof::Ed25519 {
signature: <[u8; 64]>::try_from(signature)
.expect("take(64) guarantees 64 bytes; qed"),
signer: <[u8; 32]>::try_from(signer)
.expect("take(32) guarantees 32 bytes; qed"),
},
))
}
PROOF_SECP256K1_ECDSA => {
let (input, signature) = nom::bytes::streaming::take(65u32)(input)?;
let (input, signer) = nom::bytes::streaming::take(33u32)(input)?;
Ok((
input,
Proof::Secp256k1Ecdsa {
signature: <[u8; 65]>::try_from(signature)
.expect("take(65) guarantees 65 bytes; qed"),
signer: <[u8; 33]>::try_from(signer)
.expect("take(33) guarantees 33 bytes; qed"),
},
))
}
PROOF_ON_CHAIN => {
let (input, who) = nom::bytes::streaming::take(32u32)(input)?;
let (input, block_hash) = nom::bytes::streaming::take(32u32)(input)?;
let (input, event_index) = nom::number::streaming::le_u64(input)?;
Ok((
input,
Proof::OnChain {
who: <[u8; 32]>::try_from(who).expect("take(32) guarantees 32 bytes; qed"),
block_hash: <[u8; 32]>::try_from(block_hash)
.expect("take(32) guarantees 32 bytes; qed"),
event_index,
},
))
}
_ => Err(nom::Err::Failure(nom::error::make_error(
input,
nom::error::ErrorKind::Verify,
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_decode_notification_multiple_statements() {
let statement1 = Statement {
proof: None,
decryption_key: None,
expiry: 100,
channel: None,
topics: Vec::new(),
data: Some(b"first".to_vec()),
};
let statement2 = Statement {
proof: None,
decryption_key: None,
expiry: 200,
channel: None,
topics: Vec::new(),
data: Some(b"second".to_vec()),
};
let mut encoded = Vec::new();
encoded.extend_from_slice(crate::util::encode_scale_compact_usize(2).as_ref());
encoded.extend_from_slice(&encode_statement(&statement1).unwrap());
encoded.extend_from_slice(&encode_statement(&statement2).unwrap());
let decoded = decode_statement_notification(&encoded).unwrap();
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0].1.expiry, 100);
assert_eq!(decoded[1].1.expiry, 200);
assert_eq!(decoded[0].1.data.as_deref(), Some(b"first".as_slice()));
assert_eq!(decoded[1].1.data.as_deref(), Some(b"second".as_slice()));
}
#[test]
fn complex_statement_with_all_fields() {
let signature = [0xABu8; 64];
let signer = [0xCDu8; 32];
let decryption_key = [0xEFu8; 32];
let channel = [0x12u8; 32];
let topics: Vec<[u8; 32]> = (0..MAX_TOPICS).map(|i| [i as u8; 32]).collect();
let data = vec![0x99; 5_000];
let statement = Statement {
proof: Some(Proof::Sr25519 { signature, signer }),
decryption_key: Some(decryption_key),
expiry: u64::MAX,
channel: Some(channel),
topics,
data: Some(data),
};
let encoded = encode_statement(&statement).unwrap();
let (remaining, decoded) = statement_parser(&encoded).unwrap();
assert!(remaining.is_empty());
assert_eq!(decoded.expiry, u64::MAX);
assert_eq!(decoded.topics.len(), MAX_TOPICS);
assert_eq!(decoded.data.as_ref().unwrap().len(), 5_000);
assert!(decoded.proof.is_some());
assert!(decoded.decryption_key.is_some());
assert!(decoded.channel.is_some());
}
#[test]
fn reject_out_of_order_fields() {
let mut encoded = vec![8u8]; encoded.push(FIELD_EXPIRY);
encoded.extend_from_slice(&42u64.to_le_bytes());
encoded.push(FIELD_DECRYPTION_KEY);
encoded.extend_from_slice(&[0u8; 32]);
assert!(decode_statement(&encoded).is_err());
let mut dup_expiry = vec![8u8]; dup_expiry.push(FIELD_EXPIRY);
dup_expiry.extend_from_slice(&1u64.to_le_bytes());
dup_expiry.push(FIELD_EXPIRY);
dup_expiry.extend_from_slice(&2u64.to_le_bytes());
assert!(decode_statement(&dup_expiry).is_err());
let mut dup_data = vec![8u8]; dup_data.push(FIELD_DATA);
dup_data.extend_from_slice(crate::util::encode_scale_compact_usize(1).as_ref());
dup_data.push(1u8);
dup_data.push(FIELD_DATA);
dup_data.extend_from_slice(crate::util::encode_scale_compact_usize(1).as_ref());
dup_data.push(2u8);
assert!(decode_statement(&dup_data).is_err());
let mut dup_topic = vec![16u8]; dup_topic.push(FIELD_EXPIRY);
dup_topic.extend_from_slice(&999u64.to_le_bytes());
dup_topic.push(FIELD_TOPIC_START);
dup_topic.extend_from_slice(&[0x01; 32]);
dup_topic.push(FIELD_TOPIC_START);
dup_topic.extend_from_slice(&[0x01; 32]);
dup_topic.push(FIELD_TOPIC_START + 1);
dup_topic.extend_from_slice(&[0x02; 32]);
assert!(decode_statement(&dup_topic).is_err());
let mut topic_before = vec![8u8]; topic_before.push(FIELD_TOPIC_START);
topic_before.extend_from_slice(&[0x01; 32]);
topic_before.push(FIELD_EXPIRY);
topic_before.extend_from_slice(&999u64.to_le_bytes());
assert!(decode_statement(&topic_before).is_err());
let mut data_before = vec![8u8]; data_before.push(FIELD_DATA);
data_before.extend_from_slice(crate::util::encode_scale_compact_usize(1).as_ref());
data_before.push(1u8);
data_before.push(FIELD_EXPIRY);
data_before.extend_from_slice(&42u64.to_le_bytes());
assert!(decode_statement(&data_before).is_err());
let mut channel_before = vec![8u8]; channel_before.push(FIELD_CHANNEL);
channel_before.extend_from_slice(&[0u8; 32]);
channel_before.push(FIELD_EXPIRY);
channel_before.extend_from_slice(&1u64.to_le_bytes());
assert!(decode_statement(&channel_before).is_err());
let mut topic2_before = vec![12u8]; topic2_before.push(FIELD_EXPIRY);
topic2_before.extend_from_slice(&1u64.to_le_bytes());
topic2_before.push(FIELD_TOPIC_START + 1);
topic2_before.extend_from_slice(&[0x01; 32]);
topic2_before.push(FIELD_TOPIC_START);
topic2_before.extend_from_slice(&[0x02; 32]);
assert!(decode_statement(&topic2_before).is_err());
}
#[test]
fn reject_excessive_statement_count() {
let count = MAX_STATEMENTS_PER_NOTIFICATION + 1;
let mut encoded = Vec::new();
encoded.extend_from_slice(crate::util::encode_scale_compact_usize(count).as_ref());
assert!(decode_statement_notification(&encoded).is_err());
}
#[test]
fn reject_excessive_topic_count_encoding() {
let topics: Vec<[u8; 32]> = (0..=MAX_TOPICS).map(|i| [i as u8; 32]).collect();
let statement = Statement {
proof: None,
decryption_key: None,
expiry: 0,
channel: None,
topics,
data: None,
};
let result = encode_statement(&statement);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
EncodeStatementError::TooManyTopics { got, max }
if got == MAX_TOPICS + 1 && max == MAX_TOPICS
));
}
#[test]
fn reject_statement_without_expiry() {
let mut encoded = vec![4u8];
encoded.push(4);
encoded.extend_from_slice(&[1u8; 32]);
assert!(statement_parser(&encoded).is_err());
}
#[test]
fn reject_unknown_field_tag() {
let mut encoded = vec![8u8]; encoded.push(FIELD_EXPIRY);
encoded.extend_from_slice(&42u64.to_le_bytes());
encoded.push(9); encoded.extend_from_slice(&[0u8; 32]);
assert!(decode_statement(&encoded).is_err());
let mut tag_255 = vec![4u8]; tag_255.push(255);
tag_255.extend_from_slice(&[0u8; 32]);
assert!(decode_statement(&tag_255).is_err());
let valid = encode_statement(&Statement {
proof: Some(Proof::OnChain {
who: [0u8; 32],
block_hash: [0u8; 32],
event_index: 0,
}),
decryption_key: None,
expiry: 42,
channel: None,
topics: Vec::new(),
data: None,
})
.unwrap();
assert!(decode_statement(&valid).is_ok());
let mut invalid_proof = valid.clone();
invalid_proof[2] = 99;
assert!(decode_statement(&invalid_proof).is_err());
let mut inflated = encode_statement(&Statement {
proof: None,
decryption_key: None,
expiry: 42,
channel: None,
topics: Vec::new(),
data: None,
})
.unwrap();
inflated[0] = 5 << 2; assert!(decode_statement(&inflated).is_err());
}
#[test]
fn reject_truncated_payloads() {
assert!(decode_statement(&[]).is_err());
let valid = encode_statement(&Statement {
proof: None,
decryption_key: None,
expiry: 42,
channel: None,
topics: Vec::new(),
data: None,
})
.unwrap();
assert!(decode_statement(&valid[..1]).is_err());
assert!(decode_statement(&valid[..5]).is_err());
let with_proof = encode_statement(&Statement {
proof: Some(Proof::OnChain {
who: [0u8; 32],
block_hash: [0u8; 32],
event_index: 0,
}),
decryption_key: None,
expiry: 42,
channel: None,
topics: Vec::new(),
data: None,
})
.unwrap();
assert!(decode_statement(&with_proof).is_ok());
assert!(decode_statement(&with_proof[..6]).is_err());
}
#[test]
fn decode_encoded_statement() {
let bytes = hex::decode(
"1c00032a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a181818\
1818181818181818181818181818181818181818181818181818181818420000000000000001\
dededededededededededededededededededededededededededededededede02e703000000\
00000003cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc0401\
0101010101010101010101010101010101010101010101010101010101010105020202020202\
020202020202020202020202020202020202020202020202020208083763",
)
.unwrap();
let (remaining, decoded) = statement_parser(&bytes).unwrap();
assert!(remaining.is_empty());
assert!(matches!(
decoded.proof,
Some(Proof::OnChain { who, block_hash, event_index })
if who == [42u8; 32]
&& block_hash == [24u8; 32]
&& event_index == 66
));
assert_eq!(decoded.decryption_key, Some([0xde; 32]));
assert_eq!(decoded.topics.len(), 2);
assert_eq!(decoded.topics[0], [0x01; 32]);
assert_eq!(decoded.topics[1], [0x02; 32]);
assert_eq!(decoded.data.as_deref(), Some([55, 99].as_slice()));
assert_eq!(decoded.expiry, 999);
assert_eq!(decoded.channel, Some([0xcc; 32]));
assert_eq!(encode_statement(&decoded).unwrap(), bytes);
let ed25519_bytes = hex::decode(
"0c0001aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\
bbbbbbbbbbbbbbbbbbbbbbbbbbb02e803000000000000080c010203",
)
.unwrap();
let (remaining, decoded) = statement_parser(&ed25519_bytes).unwrap();
assert!(remaining.is_empty());
assert!(matches!(
decoded.proof,
Some(Proof::Ed25519 { signature, signer })
if signature == [0xAA; 64] && signer == [0xBB; 32]
));
assert_eq!(decoded.expiry, 1000);
assert_eq!(decoded.data.as_deref(), Some([1, 2, 3].as_slice()));
assert_eq!(encode_statement(&decoded).unwrap(), ed25519_bytes);
let secp_bytes = hex::decode(
"0800027777777777777777777777777777777777777777777777777777777777777777777777777777777\
77777777777777777777777777777777777777777777777777788888888888888888888888888888888888\
888888888888888888888888888888802f401000000000000",
)
.unwrap();
let (remaining, decoded) = statement_parser(&secp_bytes).unwrap();
assert!(remaining.is_empty());
assert!(matches!(
decoded.proof,
Some(Proof::Secp256k1Ecdsa { signature, signer })
if signature == [0x77; 65] && signer == [0x88; 33]
));
assert_eq!(decoded.expiry, 500);
assert!(decoded.data.is_none());
assert_eq!(encode_statement(&decoded).unwrap(), secp_bytes);
}
#[test]
fn v2_statements_roundtrip() {
let statement1 = Statement {
proof: None,
decryption_key: None,
expiry: 100,
channel: None,
topics: Vec::new(),
data: Some(b"test1".to_vec()),
};
let statement2 = Statement {
proof: None,
decryption_key: None,
expiry: 200,
channel: None,
topics: Vec::new(),
data: Some(b"test2".to_vec()),
};
let encoded1 = encode_statement(&statement1).unwrap();
let encoded2 = encode_statement(&statement2).unwrap();
let statements: Vec<&[u8]> = vec![&encoded1, &encoded2];
let v2_encoded = encode_statements_message(&statements);
let decoded = decode_statement_message(&v2_encoded).unwrap();
match decoded {
StatementMessage::Statements(stmts) => {
assert_eq!(stmts.len(), 2);
assert_eq!(stmts[0].1, statement1);
assert_eq!(stmts[1].1, statement2);
}
_ => panic!("Expected Statements variant"),
}
}
#[test]
fn v2_statements_encoding_snapshot() {
let statement = Statement {
proof: Some(Proof::OnChain {
who: [42u8; 32],
block_hash: [24u8; 32],
event_index: 66,
}),
decryption_key: Some([0xde; 32]),
expiry: 999,
channel: Some([0xcc; 32]),
topics: vec![[0x01; 32], [0x02; 32]],
data: Some(vec![55, 99]),
};
let stmt_bytes = encode_statement(&statement).unwrap();
let v2_encoded = encode_statements_message(&[&stmt_bytes]);
let digest: [u8; 32] = blake2_rfc::blake2b::blake2b(32, &[], &v2_encoded)
.as_bytes()
.try_into()
.unwrap();
assert_eq!(
digest,
[
44, 71, 235, 73, 238, 115, 6, 15, 128, 174, 159, 216, 166, 76, 26, 101, 28, 143,
88, 21, 22, 128, 169, 62, 180, 19, 164, 234, 174, 210, 81, 105
],
"blake2_256 digest must match polkadot-sdk snapshot"
);
let decoded = decode_statement_message(&v2_encoded).unwrap();
match decoded {
StatementMessage::Statements(stmts) => {
assert_eq!(stmts.len(), 1);
assert_eq!(stmts[0].1, statement);
}
_ => panic!("Expected Statements variant"),
}
}
#[test]
fn decode_message_empty() {
assert!(matches!(
decode_statement_message(&[]),
Err(DecodeStatementMessageError::Empty)
));
}
#[test]
fn decode_message_unknown_variant() {
assert!(matches!(
decode_statement_message(&[0xFF]),
Err(DecodeStatementMessageError::UnknownVariant(0xFF))
));
}
#[test]
fn decode_message_invalid_bloom() {
assert!(matches!(
decode_statement_message(&[0x01, 0xFF]),
Err(DecodeStatementMessageError::InvalidBloomFilter)
));
}
#[test]
fn v2_affinity_encoding_snapshot() {
let topic1 = [0x01u8; 32];
let topic2 = [0x02u8; 32];
let topic3 = [0x03u8; 32];
let mut filter = AffinityFilter::new(0x5EED_5EED_5EED_5EED, 0.01, 2);
filter.insert(&topic1);
filter.insert(&topic2);
let encoded = encode_topic_affinity_message(&filter);
let digest: [u8; 32] = blake2_rfc::blake2b::blake2b(32, &[], &encoded)
.as_bytes()
.try_into()
.unwrap();
assert_eq!(
digest,
[
82, 59, 251, 163, 43, 156, 130, 249, 35, 214, 187, 99, 4, 105, 179, 131, 42, 117,
191, 57, 160, 243, 233, 20, 204, 239, 62, 120, 55, 5, 234, 62
],
"blake2_256 digest must match polkadot-sdk snapshot"
);
let decoded = decode_statement_message(&encoded).unwrap();
let StatementMessage::ExplicitTopicAffinity(af) = decoded else {
panic!("Expected ExplicitTopicAffinity variant");
};
assert!(af.contains(&topic1));
assert!(af.contains(&topic2));
assert!(!af.contains(&topic3));
}
}