1use crate::chain::{Address, Chain, Signature};
2use crate::channel::Channel;
3use crate::item_hash::{AlephItemHash, ItemHash};
4use crate::message::aggregate::AggregateContent;
5use crate::message::forget::ForgetContent;
6use crate::message::instance::InstanceContent;
7use crate::message::post::PostContent;
8use crate::message::program::ProgramContent;
9use crate::message::store::StoreContent;
10use crate::timestamp::Timestamp;
11use serde::de::{self, Deserializer};
12use serde::{Deserialize, Serialize};
13use std::fmt::Formatter;
14use thiserror::Error;
15
16#[derive(Error, Debug)]
17pub enum MessageVerificationError {
18 #[error("Item hash verification failed: expected {expected}, got {actual}")]
19 ItemHashVerificationFailed {
20 expected: ItemHash,
21 actual: ItemHash,
22 },
23 #[error("Cannot verify non-inline message locally; use the client to verify via /storage/raw/")]
24 NonInlineMessage,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
28#[serde(rename_all = "UPPERCASE")]
29pub enum MessageType {
30 Aggregate,
31 Forget,
32 Instance,
33 Post,
34 Program,
35 Store,
36}
37
38impl std::fmt::Display for MessageType {
39 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
40 let s = match self {
41 MessageType::Aggregate => "AGGREGATE",
42 MessageType::Forget => "FORGET",
43 MessageType::Instance => "INSTANCE",
44 MessageType::Post => "POST",
45 MessageType::Program => "PROGRAM",
46 MessageType::Store => "STORE",
47 };
48
49 f.write_str(s)
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54#[serde(rename_all = "lowercase")]
55pub enum MessageStatus {
56 Pending,
57 Processed,
58 Removing,
59 Removed,
60 Forgotten,
61 Rejected,
62}
63
64impl std::fmt::Display for MessageStatus {
65 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
66 let s = match self {
67 MessageStatus::Pending => "pending",
68 MessageStatus::Processed => "processed",
69 MessageStatus::Removing => "removing",
70 MessageStatus::Removed => "removed",
71 MessageStatus::Forgotten => "forgotten",
72 MessageStatus::Rejected => "rejected",
73 };
74
75 f.write_str(s)
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81#[serde(untagged)]
82pub enum MessageContentEnum {
83 Aggregate(AggregateContent),
84 Forget(ForgetContent),
85 Instance(InstanceContent),
86 Post(PostContent),
87 Program(ProgramContent),
88 Store(StoreContent),
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct MessageContent {
93 pub address: Address,
94 pub time: Timestamp,
95 #[serde(flatten)]
96 pub content: MessageContentEnum,
97}
98
99impl MessageContent {
100 pub fn deserialize_with_type(
104 message_type: MessageType,
105 raw: &[u8],
106 ) -> Result<Self, serde_json::Error> {
107 let value: serde_json::Value = serde_json::from_slice(raw)?;
108 Self::from_json_value(message_type, &value)
109 }
110
111 fn from_json_value(
112 message_type: MessageType,
113 value: &serde_json::Value,
114 ) -> Result<Self, serde_json::Error> {
115 let address = Address::deserialize(&value["address"])?;
116 let time = Timestamp::deserialize(&value["time"])?;
117
118 let variant = match message_type {
119 MessageType::Aggregate => {
120 MessageContentEnum::Aggregate(AggregateContent::deserialize(value)?)
121 }
122 MessageType::Forget => MessageContentEnum::Forget(ForgetContent::deserialize(value)?),
123 MessageType::Instance => {
124 MessageContentEnum::Instance(InstanceContent::deserialize(value)?)
125 }
126 MessageType::Post => MessageContentEnum::Post(PostContent::deserialize(value)?),
127 MessageType::Program => {
128 MessageContentEnum::Program(ProgramContent::deserialize(value)?)
129 }
130 MessageType::Store => MessageContentEnum::Store(StoreContent::deserialize(value)?),
131 };
132
133 Ok(MessageContent {
134 address,
135 time,
136 content: variant,
137 })
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142pub struct MessageConfirmation {
143 pub chain: Chain,
144 pub height: u64,
145 pub hash: String,
146 pub time: Option<Timestamp>,
147 pub publisher: Option<Address>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
153#[serde(rename_all = "lowercase")]
154pub enum ContentSource {
155 Inline { item_content: String },
156 Storage,
157 Ipfs,
158}
159
160impl<'de> Deserialize<'de> for ContentSource {
161 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162 where
163 D: Deserializer<'de>,
164 {
165 #[derive(Deserialize)]
166 struct ContentSourceRaw {
167 item_type: String,
168 item_content: Option<String>,
169 }
170
171 let raw = ContentSourceRaw::deserialize(deserializer)?;
172
173 match raw.item_type.as_str() {
174 "inline" => {
175 let item_content = raw
176 .item_content
177 .ok_or_else(|| de::Error::missing_field("item_content"))?;
178 Ok(ContentSource::Inline { item_content })
179 }
180 "storage" => Ok(ContentSource::Storage),
181 "ipfs" => Ok(ContentSource::Ipfs),
182 other => Err(de::Error::unknown_variant(
183 other,
184 &["inline", "storage", "ipfs"],
185 )),
186 }
187 }
188}
189
190impl ContentSource {
191 pub fn verify_inline_hash(
196 &self,
197 expected_hash: &ItemHash,
198 ) -> Option<Result<(), (ItemHash, ItemHash)>> {
199 match self {
200 ContentSource::Inline { item_content } => {
201 let computed = AlephItemHash::from_bytes(item_content.as_bytes());
202 if ItemHash::Native(computed) != *expected_hash {
203 Some(Err((expected_hash.clone(), computed.into())))
204 } else {
205 Some(Ok(()))
206 }
207 }
208 ContentSource::Storage | ContentSource::Ipfs => None,
209 }
210 }
211}
212
213#[derive(PartialEq, Debug, Clone)]
219pub struct MessageHeader {
220 pub chain: Chain,
222 pub sender: Address,
224 pub signature: Signature,
226 pub content_source: ContentSource,
229 pub item_hash: ItemHash,
231 pub confirmations: Vec<MessageConfirmation>,
233 pub time: Timestamp,
235 pub channel: Option<Channel>,
237 pub message_type: MessageType,
239}
240
241impl MessageHeader {
242 pub fn with_content(self, content: MessageContent) -> Message {
244 Message {
245 chain: self.chain,
246 sender: self.sender,
247 signature: self.signature,
248 content_source: self.content_source,
249 item_hash: self.item_hash,
250 confirmations: self.confirmations,
251 time: self.time,
252 channel: self.channel,
253 message_type: self.message_type,
254 content,
255 }
256 }
257
258 #[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
264 pub fn verify_signature(
265 &self,
266 ) -> Result<(), crate::verify_signature::SignatureVerificationError> {
267 crate::verify_signature::verify(
268 &self.chain,
269 &self.sender,
270 &self.signature,
271 self.message_type,
272 &self.item_hash,
273 )
274 }
275}
276
277impl From<Message> for MessageHeader {
278 fn from(message: Message) -> Self {
279 MessageHeader {
280 chain: message.chain,
281 sender: message.sender,
282 signature: message.signature,
283 content_source: message.content_source,
284 item_hash: message.item_hash,
285 confirmations: message.confirmations,
286 time: message.time,
287 channel: message.channel,
288 message_type: message.message_type,
289 }
290 }
291}
292
293#[derive(PartialEq, Debug, Clone)]
294pub struct Message {
295 pub chain: Chain,
297 pub sender: Address,
299 pub signature: Signature,
301 pub content_source: ContentSource,
304 pub item_hash: ItemHash,
306 pub confirmations: Vec<MessageConfirmation>,
308 pub time: Timestamp,
310 pub channel: Option<Channel>,
312 pub message_type: MessageType,
314 pub content: MessageContent,
316}
317
318impl Message {
319 pub fn content(&self) -> &MessageContentEnum {
320 &self.content.content
321 }
322
323 pub fn confirmed(&self) -> bool {
324 !self.confirmations.is_empty()
325 }
326
327 pub fn sender(&self) -> &Address {
331 &self.sender
332 }
333
334 pub fn owner(&self) -> &Address {
336 &self.content.address
337 }
338
339 pub fn sent_at(&self) -> &Timestamp {
344 &self.content.time
345 }
346
347 pub fn confirmed_at(&self) -> Option<&Timestamp> {
349 self.confirmations.first().and_then(|c| c.time.as_ref())
350 }
351
352 pub fn verify_item_hash(&self) -> Result<(), MessageVerificationError> {
358 match self.content_source.verify_inline_hash(&self.item_hash) {
359 Some(Ok(())) => Ok(()),
360 Some(Err((expected, actual))) => {
361 Err(MessageVerificationError::ItemHashVerificationFailed { expected, actual })
362 }
363 None => Err(MessageVerificationError::NonInlineMessage),
364 }
365 }
366
367 #[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
374 pub fn verify_signature(
375 &self,
376 ) -> Result<(), crate::verify_signature::SignatureVerificationError> {
377 crate::verify_signature::verify(
378 &self.chain,
379 &self.sender,
380 &self.signature,
381 self.message_type,
382 &self.item_hash,
383 )
384 }
385}
386
387#[derive(Deserialize)]
390struct MessageHeaderRaw {
391 chain: Chain,
392 sender: Address,
393 signature: Signature,
394 #[serde(flatten)]
395 content_source: ContentSource,
396 item_hash: ItemHash,
397 #[serde(default)]
398 confirmations: Option<Vec<MessageConfirmation>>,
399 time: Timestamp,
400 #[serde(default)]
401 channel: Option<Channel>,
402 #[serde(rename = "type")]
403 message_type: MessageType,
404}
405
406impl MessageHeaderRaw {
407 fn into_header(self) -> MessageHeader {
408 MessageHeader {
409 chain: self.chain,
410 sender: self.sender,
411 signature: self.signature,
412 content_source: self.content_source,
413 item_hash: self.item_hash,
414 confirmations: self.confirmations.unwrap_or_default(),
415 time: self.time,
416 channel: self.channel,
417 message_type: self.message_type,
418 }
419 }
420}
421
422impl<'de> Deserialize<'de> for MessageHeader {
423 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
424 where
425 D: Deserializer<'de>,
426 {
427 MessageHeaderRaw::deserialize(deserializer).map(MessageHeaderRaw::into_header)
428 }
429}
430
431impl<'de> Deserialize<'de> for Message {
433 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
434 where
435 D: Deserializer<'de>,
436 {
437 #[derive(Deserialize)]
438 struct MessageRaw {
439 #[serde(flatten)]
440 header: MessageHeaderRaw,
441 content: serde_json::Value,
442 }
443
444 let raw = MessageRaw::deserialize(deserializer)?;
445
446 let content = MessageContent::from_json_value(raw.header.message_type, &raw.content)
447 .map_err(de::Error::custom)?;
448
449 Ok(raw.header.into_header().with_content(content))
450 }
451}
452
453impl Serialize for Message {
455 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
456 where
457 S: serde::Serializer,
458 {
459 use serde::ser::SerializeStruct;
460
461 let mut state = serializer.serialize_struct("Message", 9)?;
462 state.serialize_field("chain", &self.chain)?;
463 state.serialize_field("sender", &self.sender)?;
464 match &self.content_source {
465 ContentSource::Inline { item_content } => {
466 state.serialize_field("item_type", "inline")?;
467 state.serialize_field("item_content", item_content)?;
468 }
469 ContentSource::Storage => {
470 state.serialize_field("item_type", "storage")?;
471 state.serialize_field("item_content", &None::<String>)?;
472 }
473 ContentSource::Ipfs => {
474 state.serialize_field("item_type", "ipfs")?;
475 state.serialize_field("item_content", &None::<String>)?;
476 }
477 }
478 state.serialize_field("signature", &self.signature)?;
479 state.serialize_field("item_hash", &self.item_hash)?;
480 if self.confirmed() {
481 state.serialize_field("confirmed", &true)?;
482 state.serialize_field("confirmations", &self.confirmations)?;
483 }
484 state.serialize_field("time", &self.time)?;
485 if self.channel.is_some() {
486 state.serialize_field("channel", &self.channel)?;
487 }
488 state.serialize_field("type", &self.message_type)?;
489 state.serialize_field("content", &self.content)?;
490 state.end()
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::item_hash;
498 use assert_matches::assert_matches;
499
500 #[test]
501 fn test_deserialize_item_type_inline() {
502 let item_content_str = "test".to_string();
503 let content_source_str =
504 format!("{{\"item_type\":\"inline\",\"item_content\":\"{item_content_str}\"}}");
505 let content_source: ContentSource = serde_json::from_str(&content_source_str).unwrap();
506
507 assert_matches!(
508 content_source,
509 ContentSource::Inline {
510 item_content
511 } if item_content == item_content_str
512 );
513 }
514
515 #[test]
516 fn test_deserialize_item_type_storage() {
517 let content_source_str = r#"{"item_type":"storage"}"#;
518 let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
519 assert_matches!(content_source, ContentSource::Storage);
520 }
521
522 #[test]
523 fn test_deserialize_item_type_ipfs() {
524 let content_source_str = r#"{"item_type":"ipfs"}"#;
525 let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
526 assert_matches!(content_source, ContentSource::Ipfs);
527 }
528
529 #[test]
530 fn test_verify_inline_message_item_hash() {
531 let json = include_str!("../../../../fixtures/messages/post/post.json");
532 let message: Message = serde_json::from_str(json).unwrap();
533 message.verify_item_hash().unwrap();
534
535 let json = include_str!("../../../../fixtures/messages/store/store-ipfs.json");
537 let message: Message = serde_json::from_str(json).unwrap();
538 message.verify_item_hash().unwrap();
539 }
540
541 #[test]
542 fn test_verify_inline_message_detects_tampered_hash() {
543 let json = include_str!("../../../../fixtures/messages/post/post.json");
544 let mut message: Message = serde_json::from_str(json).unwrap();
545 message.item_hash =
546 item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
547 assert_matches!(
548 message.verify_item_hash(),
549 Err(MessageVerificationError::ItemHashVerificationFailed { .. })
550 );
551 }
552
553 #[test]
554 fn test_verify_inline_message_detects_tampered_content() {
555 let json = include_str!("../../../../fixtures/messages/post/post.json");
556 let mut message: Message = serde_json::from_str(json).unwrap();
557 if let ContentSource::Inline {
559 ref mut item_content,
560 } = message.content_source
561 {
562 item_content.push('!');
563 }
564 assert_matches!(
565 message.verify_item_hash(),
566 Err(MessageVerificationError::ItemHashVerificationFailed { .. })
567 );
568 }
569
570 #[test]
571 fn test_verify_non_inline_message_returns_error() {
572 let json = include_str!("../../../../fixtures/messages/aggregate/aggregate.json");
573 let message: Message = serde_json::from_str(json).unwrap();
574 assert_matches!(
575 message.verify_item_hash(),
576 Err(MessageVerificationError::NonInlineMessage)
577 );
578 }
579
580 #[test]
581 fn test_deserialize_message_header() {
582 let json = include_str!("../../../../fixtures/messages/post/post.json");
583 let header: MessageHeader = serde_json::from_str(json).unwrap();
584 let message: Message = serde_json::from_str(json).unwrap();
585
586 assert_eq!(header.chain, message.chain);
588 assert_eq!(header.sender, message.sender);
589 assert_eq!(header.signature, message.signature);
590 assert_eq!(header.content_source, message.content_source);
591 assert_eq!(header.item_hash, message.item_hash);
592 assert_eq!(header.time, message.time);
593 assert_eq!(header.channel, message.channel);
594 assert_eq!(header.message_type, message.message_type);
595 }
596
597 #[test]
598 fn test_message_header_with_content_roundtrip() {
599 let json = include_str!("../../../../fixtures/messages/post/post.json");
600 let message: Message = serde_json::from_str(json).unwrap();
601
602 let content = message.content.clone();
604 let header = MessageHeader::from(message.clone());
605 let reassembled = header.with_content(content);
606 assert_eq!(reassembled, message);
607 }
608
609 #[test]
610 fn test_deserialize_content_with_type() {
611 let json = include_str!("../../../../fixtures/messages/post/post.json");
612 let message: Message = serde_json::from_str(json).unwrap();
613
614 if let ContentSource::Inline { ref item_content } = message.content_source {
616 let content = MessageContent::deserialize_with_type(
617 message.message_type,
618 item_content.as_bytes(),
619 )
620 .unwrap();
621 assert_eq!(content, message.content);
622 } else {
623 panic!("Expected inline message");
624 }
625 }
626
627 #[test]
628 fn test_deserialize_item_type_invalid_type() {
629 let content_source_str = r#"{"item_type":"invalid"}"#;
630 let result = serde_json::from_str::<ContentSource>(content_source_str);
631 assert!(result.is_err());
632 }
633
634 #[test]
635 fn test_deserialize_item_type_invalid_format() {
636 let content_source_str = r#"{"type":"inline"}"#;
637 let result = serde_json::from_str::<ContentSource>(content_source_str);
638 assert!(result.is_err());
639 }
640
641 #[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
642 mod signature_tests {
643 use super::*;
644 use crate::verify_signature::SignatureVerificationError;
645
646 #[test]
647 fn test_verify_signature_unsupported_chain() {
648 let json = include_str!("../../../../fixtures/messages/post/post.json");
649 let mut message: Message = serde_json::from_str(json).unwrap();
650 message.chain = Chain::Tezos;
651 assert_matches!(
652 message.verify_signature(),
653 Err(SignatureVerificationError::UnsupportedChain(_))
654 );
655 }
656
657 #[cfg(feature = "signature-evm")]
658 mod evm {
659 use super::*;
660 use crate::chain::Signature;
661
662 fn post_message() -> Message {
663 let json = include_str!("../../../../fixtures/messages/post/post.json");
664 serde_json::from_str(json).unwrap()
665 }
666
667 #[test]
668 fn test_verify_signature_valid() {
669 let message = post_message();
670 message.verify_signature().unwrap();
671 }
672
673 #[test]
674 fn test_verify_signature_tampered_sender() {
675 let mut message = post_message();
676 message.sender =
677 Address::from("0x0000000000000000000000000000000000000000".to_string());
678 assert_matches!(
679 message.verify_signature(),
680 Err(SignatureVerificationError::SignatureMismatch { .. })
681 );
682 }
683
684 #[test]
685 fn test_verify_signature_tampered_item_hash() {
686 let mut message = post_message();
687 message.item_hash =
688 item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
689 assert_matches!(
690 message.verify_signature(),
691 Err(SignatureVerificationError::SignatureMismatch { .. })
692 );
693 }
694
695 #[test]
696 fn test_verify_signature_invalid_hex() {
697 let mut message = post_message();
698 message.signature = Signature::from("not-a-hex-string".to_string());
699 assert_matches!(
700 message.verify_signature(),
701 Err(SignatureVerificationError::InvalidSignature(_))
702 );
703 }
704
705 #[test]
706 fn test_verify_signature_wrong_but_valid_signature() {
707 let mut message = post_message();
708 message.signature = Signature::from(
709 "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001\
710 00"
711 .to_string(),
712 );
713 assert_matches!(
715 message.verify_signature(),
716 Err(SignatureVerificationError::SignatureMismatch { .. }
717 | SignatureVerificationError::InvalidSignature(_))
718 );
719 }
720
721 #[test]
722 fn test_verify_signature_evm_chain_dispatch() {
723 let mut message = post_message();
724 message.chain = Chain::Arbitrum;
725 assert_matches!(
728 message.verify_signature(),
729 Err(SignatureVerificationError::SignatureMismatch { .. })
730 );
731 }
732 }
733
734 #[cfg(feature = "signature-sol")]
735 mod sol {
736 use super::*;
737
738 fn sol_post_message() -> Message {
739 let json = include_str!("../../../../fixtures/messages/post/post-sol.json");
740 serde_json::from_str(json).unwrap()
741 }
742
743 #[test]
744 fn test_verify_sol_signature_valid() {
745 let message = sol_post_message();
746 message.verify_signature().unwrap();
747 }
748
749 #[test]
750 fn test_verify_sol_signature_tampered_sender() {
751 let mut message = sol_post_message();
752 message.sender = Address::from("11111111111111111111111111111111".to_string());
753 assert_matches!(
754 message.verify_signature(),
755 Err(SignatureVerificationError::InvalidSignature(_))
756 );
757 }
758
759 #[test]
760 fn test_verify_sol_signature_tampered_item_hash() {
761 let mut message = sol_post_message();
762 message.item_hash =
763 item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
764 assert_matches!(
765 message.verify_signature(),
766 Err(SignatureVerificationError::InvalidSignature(_))
767 );
768 }
769
770 #[test]
771 fn test_deserialize_sol_signature_format() {
772 let message = sol_post_message();
773 assert!(message.signature.public_key().is_some());
774 assert_eq!(
775 message.signature.public_key().unwrap(),
776 "5SwCeHbZ9oY3556YFBEhPTHyy9t4yse26v7MUyGm2bHS"
777 );
778 }
779 }
780 }
781}