use crate::chain::{Address, Chain, Signature};
use crate::channel::Channel;
use crate::item_hash::{AlephItemHash, ItemHash};
use crate::message::aggregate::AggregateContent;
use crate::message::forget::ForgetContent;
use crate::message::instance::InstanceContent;
use crate::message::post::PostContent;
use crate::message::program::ProgramContent;
use crate::message::store::StoreContent;
use crate::timestamp::Timestamp;
use serde::de::{self, Deserializer};
use serde::{Deserialize, Serialize};
use std::fmt::Formatter;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MessageVerificationError {
#[error("Item hash verification failed: expected {expected}, got {actual}")]
ItemHashVerificationFailed {
expected: ItemHash,
actual: ItemHash,
},
#[error("Cannot verify non-inline message locally; use the client to verify via /storage/raw/")]
NonInlineMessage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum MessageType {
Aggregate,
Forget,
Instance,
Post,
Program,
Store,
}
impl std::fmt::Display for MessageType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
MessageType::Aggregate => "AGGREGATE",
MessageType::Forget => "FORGET",
MessageType::Instance => "INSTANCE",
MessageType::Post => "POST",
MessageType::Program => "PROGRAM",
MessageType::Store => "STORE",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageStatus {
Pending,
Processed,
Removing,
Removed,
Forgotten,
Rejected,
}
impl std::fmt::Display for MessageStatus {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = match self {
MessageStatus::Pending => "pending",
MessageStatus::Processed => "processed",
MessageStatus::Removing => "removing",
MessageStatus::Removed => "removed",
MessageStatus::Forgotten => "forgotten",
MessageStatus::Rejected => "rejected",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContentEnum {
Aggregate(AggregateContent),
Forget(ForgetContent),
Instance(InstanceContent),
Post(PostContent),
Program(ProgramContent),
Store(StoreContent),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MessageContent {
pub address: Address,
pub time: Timestamp,
#[serde(flatten)]
pub content: MessageContentEnum,
}
impl MessageContent {
pub fn deserialize_with_type(
message_type: MessageType,
raw: &[u8],
) -> Result<Self, serde_json::Error> {
let value: serde_json::Value = serde_json::from_slice(raw)?;
Self::from_json_value(message_type, &value)
}
fn from_json_value(
message_type: MessageType,
value: &serde_json::Value,
) -> Result<Self, serde_json::Error> {
let address = Address::deserialize(&value["address"])?;
let time = Timestamp::deserialize(&value["time"])?;
let variant = match message_type {
MessageType::Aggregate => {
MessageContentEnum::Aggregate(AggregateContent::deserialize(value)?)
}
MessageType::Forget => MessageContentEnum::Forget(ForgetContent::deserialize(value)?),
MessageType::Instance => {
MessageContentEnum::Instance(InstanceContent::deserialize(value)?)
}
MessageType::Post => MessageContentEnum::Post(PostContent::deserialize(value)?),
MessageType::Program => {
MessageContentEnum::Program(ProgramContent::deserialize(value)?)
}
MessageType::Store => MessageContentEnum::Store(StoreContent::deserialize(value)?),
};
Ok(MessageContent {
address,
time,
content: variant,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MessageConfirmation {
pub chain: Chain,
pub height: u64,
pub hash: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time: Option<Timestamp>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub publisher: Option<Address>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ContentSource {
Inline { item_content: String },
Storage,
Ipfs,
}
impl<'de> Deserialize<'de> for ContentSource {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct ContentSourceRaw {
item_type: String,
item_content: Option<String>,
}
let raw = ContentSourceRaw::deserialize(deserializer)?;
match raw.item_type.as_str() {
"inline" => {
let item_content = raw
.item_content
.ok_or_else(|| de::Error::missing_field("item_content"))?;
Ok(ContentSource::Inline { item_content })
}
"storage" => Ok(ContentSource::Storage),
"ipfs" => Ok(ContentSource::Ipfs),
other => Err(de::Error::unknown_variant(
other,
&["inline", "storage", "ipfs"],
)),
}
}
}
impl ContentSource {
pub fn verify_inline_hash(
&self,
expected_hash: &ItemHash,
) -> Option<Result<(), (ItemHash, ItemHash)>> {
match self {
ContentSource::Inline { item_content } => {
let computed = AlephItemHash::from_bytes(item_content.as_bytes());
if ItemHash::Native(computed) != *expected_hash {
Some(Err((expected_hash.clone(), computed.into())))
} else {
Some(Ok(()))
}
}
ContentSource::Storage | ContentSource::Ipfs => None,
}
}
}
#[derive(PartialEq, Debug, Clone)]
pub struct MessageHeader {
pub chain: Chain,
pub sender: Address,
pub signature: Signature,
pub content_source: ContentSource,
pub item_hash: ItemHash,
pub confirmations: Vec<MessageConfirmation>,
pub time: Timestamp,
pub channel: Option<Channel>,
pub message_type: MessageType,
}
impl MessageHeader {
pub fn with_content(self, content: MessageContent) -> Message {
Message {
chain: self.chain,
sender: self.sender,
signature: self.signature,
content_source: self.content_source,
item_hash: self.item_hash,
confirmations: self.confirmations,
time: self.time,
channel: self.channel,
message_type: self.message_type,
content,
}
}
#[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
pub fn verify_signature(
&self,
) -> Result<(), crate::verify_signature::SignatureVerificationError> {
crate::verify_signature::verify(
&self.chain,
&self.sender,
&self.signature,
self.message_type,
&self.item_hash,
)
}
}
impl From<Message> for MessageHeader {
fn from(message: Message) -> Self {
MessageHeader {
chain: message.chain,
sender: message.sender,
signature: message.signature,
content_source: message.content_source,
item_hash: message.item_hash,
confirmations: message.confirmations,
time: message.time,
channel: message.channel,
message_type: message.message_type,
}
}
}
#[derive(PartialEq, Debug, Clone)]
pub struct Message {
pub chain: Chain,
pub sender: Address,
pub signature: Signature,
pub content_source: ContentSource,
pub item_hash: ItemHash,
pub confirmations: Vec<MessageConfirmation>,
pub time: Timestamp,
pub channel: Option<Channel>,
pub message_type: MessageType,
pub content: MessageContent,
}
impl Message {
pub fn content(&self) -> &MessageContentEnum {
&self.content.content
}
pub fn confirmed(&self) -> bool {
!self.confirmations.is_empty()
}
pub fn sender(&self) -> &Address {
&self.sender
}
pub fn owner(&self) -> &Address {
&self.content.address
}
pub fn sent_at(&self) -> &Timestamp {
&self.content.time
}
pub fn confirmed_at(&self) -> Option<&Timestamp> {
self.confirmations.first().and_then(|c| c.time.as_ref())
}
pub fn verify_item_hash(&self) -> Result<(), MessageVerificationError> {
match self.content_source.verify_inline_hash(&self.item_hash) {
Some(Ok(())) => Ok(()),
Some(Err((expected, actual))) => {
Err(MessageVerificationError::ItemHashVerificationFailed { expected, actual })
}
None => Err(MessageVerificationError::NonInlineMessage),
}
}
#[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
pub fn verify_signature(
&self,
) -> Result<(), crate::verify_signature::SignatureVerificationError> {
crate::verify_signature::verify(
&self.chain,
&self.sender,
&self.signature,
self.message_type,
&self.item_hash,
)
}
}
#[derive(Deserialize)]
struct MessageHeaderRaw {
chain: Chain,
sender: Address,
signature: Signature,
#[serde(flatten)]
content_source: ContentSource,
item_hash: ItemHash,
#[serde(default)]
confirmations: Option<Vec<MessageConfirmation>>,
time: Timestamp,
#[serde(default)]
channel: Option<Channel>,
#[serde(rename = "type")]
message_type: MessageType,
}
impl MessageHeaderRaw {
fn into_header(self) -> MessageHeader {
MessageHeader {
chain: self.chain,
sender: self.sender,
signature: self.signature,
content_source: self.content_source,
item_hash: self.item_hash,
confirmations: self.confirmations.unwrap_or_default(),
time: self.time,
channel: self.channel,
message_type: self.message_type,
}
}
}
impl<'de> Deserialize<'de> for MessageHeader {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
MessageHeaderRaw::deserialize(deserializer).map(MessageHeaderRaw::into_header)
}
}
impl<'de> Deserialize<'de> for Message {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct MessageRaw {
#[serde(flatten)]
header: MessageHeaderRaw,
content: serde_json::Value,
}
let raw = MessageRaw::deserialize(deserializer)?;
let content = MessageContent::from_json_value(raw.header.message_type, &raw.content)
.map_err(de::Error::custom)?;
Ok(raw.header.into_header().with_content(content))
}
}
impl Serialize for Message {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("Message", 9)?;
state.serialize_field("chain", &self.chain)?;
state.serialize_field("sender", &self.sender)?;
match &self.content_source {
ContentSource::Inline { item_content } => {
state.serialize_field("item_type", "inline")?;
state.serialize_field("item_content", item_content)?;
}
ContentSource::Storage => {
state.serialize_field("item_type", "storage")?;
state.serialize_field("item_content", &None::<String>)?;
}
ContentSource::Ipfs => {
state.serialize_field("item_type", "ipfs")?;
state.serialize_field("item_content", &None::<String>)?;
}
}
state.serialize_field("signature", &self.signature)?;
state.serialize_field("item_hash", &self.item_hash)?;
if self.confirmed() {
state.serialize_field("confirmed", &true)?;
state.serialize_field("confirmations", &self.confirmations)?;
}
state.serialize_field("time", &self.time)?;
if self.channel.is_some() {
state.serialize_field("channel", &self.channel)?;
}
state.serialize_field("type", &self.message_type)?;
state.serialize_field("content", &self.content)?;
state.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::item_hash;
use assert_matches::assert_matches;
#[test]
fn test_deserialize_item_type_inline() {
let item_content_str = "test".to_string();
let content_source_str =
format!("{{\"item_type\":\"inline\",\"item_content\":\"{item_content_str}\"}}");
let content_source: ContentSource = serde_json::from_str(&content_source_str).unwrap();
assert_matches!(
content_source,
ContentSource::Inline {
item_content
} if item_content == item_content_str
);
}
#[test]
fn test_deserialize_item_type_storage() {
let content_source_str = r#"{"item_type":"storage"}"#;
let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
assert_matches!(content_source, ContentSource::Storage);
}
#[test]
fn test_deserialize_item_type_ipfs() {
let content_source_str = r#"{"item_type":"ipfs"}"#;
let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
assert_matches!(content_source, ContentSource::Ipfs);
}
#[test]
fn test_verify_inline_message_item_hash() {
let json = include_str!("../../../../fixtures/messages/post/post.json");
let message: Message = serde_json::from_str(json).unwrap();
message.verify_item_hash().unwrap();
let json = include_str!("../../../../fixtures/messages/store/store-ipfs.json");
let message: Message = serde_json::from_str(json).unwrap();
message.verify_item_hash().unwrap();
}
#[test]
fn test_verify_inline_message_detects_tampered_hash() {
let json = include_str!("../../../../fixtures/messages/post/post.json");
let mut message: Message = serde_json::from_str(json).unwrap();
message.item_hash =
item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
assert_matches!(
message.verify_item_hash(),
Err(MessageVerificationError::ItemHashVerificationFailed { .. })
);
}
#[test]
fn test_verify_inline_message_detects_tampered_content() {
let json = include_str!("../../../../fixtures/messages/post/post.json");
let mut message: Message = serde_json::from_str(json).unwrap();
if let ContentSource::Inline {
ref mut item_content,
} = message.content_source
{
item_content.push('!');
}
assert_matches!(
message.verify_item_hash(),
Err(MessageVerificationError::ItemHashVerificationFailed { .. })
);
}
#[test]
fn test_verify_non_inline_message_returns_error() {
let json = include_str!("../../../../fixtures/messages/aggregate/aggregate.json");
let message: Message = serde_json::from_str(json).unwrap();
assert_matches!(
message.verify_item_hash(),
Err(MessageVerificationError::NonInlineMessage)
);
}
#[test]
fn test_deserialize_message_header() {
let json = include_str!("../../../../fixtures/messages/post/post.json");
let header: MessageHeader = serde_json::from_str(json).unwrap();
let message: Message = serde_json::from_str(json).unwrap();
assert_eq!(header.chain, message.chain);
assert_eq!(header.sender, message.sender);
assert_eq!(header.signature, message.signature);
assert_eq!(header.content_source, message.content_source);
assert_eq!(header.item_hash, message.item_hash);
assert_eq!(header.time, message.time);
assert_eq!(header.channel, message.channel);
assert_eq!(header.message_type, message.message_type);
}
#[test]
fn test_message_header_with_content_roundtrip() {
let json = include_str!("../../../../fixtures/messages/post/post.json");
let message: Message = serde_json::from_str(json).unwrap();
let content = message.content.clone();
let header = MessageHeader::from(message.clone());
let reassembled = header.with_content(content);
assert_eq!(reassembled, message);
}
#[test]
fn test_deserialize_content_with_type() {
let json = include_str!("../../../../fixtures/messages/post/post.json");
let message: Message = serde_json::from_str(json).unwrap();
if let ContentSource::Inline { ref item_content } = message.content_source {
let content = MessageContent::deserialize_with_type(
message.message_type,
item_content.as_bytes(),
)
.unwrap();
assert_eq!(content, message.content);
} else {
panic!("Expected inline message");
}
}
#[test]
fn test_deserialize_item_type_invalid_type() {
let content_source_str = r#"{"item_type":"invalid"}"#;
let result = serde_json::from_str::<ContentSource>(content_source_str);
assert!(result.is_err());
}
#[test]
fn test_deserialize_item_type_invalid_format() {
let content_source_str = r#"{"type":"inline"}"#;
let result = serde_json::from_str::<ContentSource>(content_source_str);
assert!(result.is_err());
}
#[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
mod signature_tests {
use super::*;
use crate::verify_signature::SignatureVerificationError;
#[test]
fn test_verify_signature_unsupported_chain() {
let json = include_str!("../../../../fixtures/messages/post/post.json");
let mut message: Message = serde_json::from_str(json).unwrap();
message.chain = Chain::Tezos;
assert_matches!(
message.verify_signature(),
Err(SignatureVerificationError::UnsupportedChain(_))
);
}
#[cfg(feature = "signature-evm")]
mod evm {
use super::*;
use crate::chain::Signature;
fn post_message() -> Message {
let json = include_str!("../../../../fixtures/messages/post/post.json");
serde_json::from_str(json).unwrap()
}
#[test]
fn test_verify_signature_valid() {
let message = post_message();
message.verify_signature().unwrap();
}
#[test]
fn test_verify_signature_tampered_sender() {
let mut message = post_message();
message.sender =
Address::from("0x0000000000000000000000000000000000000000".to_string());
assert_matches!(
message.verify_signature(),
Err(SignatureVerificationError::SignatureMismatch { .. })
);
}
#[test]
fn test_verify_signature_tampered_item_hash() {
let mut message = post_message();
message.item_hash =
item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
assert_matches!(
message.verify_signature(),
Err(SignatureVerificationError::SignatureMismatch { .. })
);
}
#[test]
fn test_verify_signature_invalid_hex() {
let mut message = post_message();
message.signature = Signature::from("not-a-hex-string".to_string());
assert_matches!(
message.verify_signature(),
Err(SignatureVerificationError::InvalidSignature(_))
);
}
#[test]
fn test_verify_signature_wrong_but_valid_signature() {
let mut message = post_message();
message.signature = Signature::from(
"0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001\
00"
.to_string(),
);
assert_matches!(
message.verify_signature(),
Err(SignatureVerificationError::SignatureMismatch { .. }
| SignatureVerificationError::InvalidSignature(_))
);
}
#[test]
fn test_verify_signature_evm_chain_dispatch() {
let mut message = post_message();
message.chain = Chain::Arbitrum;
assert_matches!(
message.verify_signature(),
Err(SignatureVerificationError::SignatureMismatch { .. })
);
}
}
#[cfg(feature = "signature-sol")]
mod sol {
use super::*;
fn sol_post_message() -> Message {
let json = include_str!("../../../../fixtures/messages/post/post-sol.json");
serde_json::from_str(json).unwrap()
}
#[test]
fn test_verify_sol_signature_valid() {
let message = sol_post_message();
message.verify_signature().unwrap();
}
#[test]
fn test_verify_sol_signature_tampered_sender() {
let mut message = sol_post_message();
message.sender = Address::from("11111111111111111111111111111111".to_string());
assert_matches!(
message.verify_signature(),
Err(SignatureVerificationError::InvalidSignature(_))
);
}
#[test]
fn test_verify_sol_signature_tampered_item_hash() {
let mut message = sol_post_message();
message.item_hash =
item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
assert_matches!(
message.verify_signature(),
Err(SignatureVerificationError::InvalidSignature(_))
);
}
#[test]
fn test_deserialize_sol_signature_format() {
let message = sol_post_message();
assert!(message.signature.public_key().is_some());
assert_eq!(
message.signature.public_key().unwrap(),
"5SwCeHbZ9oY3556YFBEhPTHyy9t4yse26v7MUyGm2bHS"
);
}
}
}
}