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, Eq, PartialOrd, Ord, Hash, 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 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub time: Option<Timestamp>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub publisher: Option<Address>,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
155#[serde(rename_all = "lowercase")]
156pub enum ContentSource {
157 Inline { item_content: String },
158 Storage,
159 Ipfs,
160}
161
162impl<'de> Deserialize<'de> for ContentSource {
163 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164 where
165 D: Deserializer<'de>,
166 {
167 #[derive(Deserialize)]
168 struct ContentSourceRaw {
169 item_type: String,
170 item_content: Option<String>,
171 }
172
173 let raw = ContentSourceRaw::deserialize(deserializer)?;
174
175 match raw.item_type.as_str() {
176 "inline" => {
177 let item_content = raw
178 .item_content
179 .ok_or_else(|| de::Error::missing_field("item_content"))?;
180 Ok(ContentSource::Inline { item_content })
181 }
182 "storage" => Ok(ContentSource::Storage),
183 "ipfs" => Ok(ContentSource::Ipfs),
184 other => Err(de::Error::unknown_variant(
185 other,
186 &["inline", "storage", "ipfs"],
187 )),
188 }
189 }
190}
191
192impl ContentSource {
193 pub fn verify_inline_hash(
198 &self,
199 expected_hash: &ItemHash,
200 ) -> Option<Result<(), (ItemHash, ItemHash)>> {
201 match self {
202 ContentSource::Inline { item_content } => {
203 let computed = AlephItemHash::from_bytes(item_content.as_bytes());
204 if ItemHash::Native(computed) != *expected_hash {
205 Some(Err((expected_hash.clone(), computed.into())))
206 } else {
207 Some(Ok(()))
208 }
209 }
210 ContentSource::Storage | ContentSource::Ipfs => None,
211 }
212 }
213}
214
215#[derive(PartialEq, Debug, Clone)]
221pub struct MessageHeader {
222 pub chain: Chain,
224 pub sender: Address,
226 pub signature: Option<Signature>,
232 pub content_source: ContentSource,
235 pub item_hash: ItemHash,
237 pub confirmations: Vec<MessageConfirmation>,
239 pub time: Timestamp,
241 pub channel: Option<Channel>,
243 pub message_type: MessageType,
245}
246
247impl MessageHeader {
248 pub fn with_content(self, content: MessageContent) -> Message {
250 Message {
251 chain: self.chain,
252 sender: self.sender,
253 signature: self.signature,
254 content_source: self.content_source,
255 item_hash: self.item_hash,
256 confirmations: self.confirmations,
257 time: self.time,
258 channel: self.channel,
259 message_type: self.message_type,
260 content,
261 }
262 }
263
264 #[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
270 pub fn verify_signature(
271 &self,
272 ) -> Result<(), crate::verify_signature::SignatureVerificationError> {
273 let signature = self
274 .signature
275 .as_ref()
276 .ok_or(crate::verify_signature::SignatureVerificationError::MissingSignature)?;
277 crate::verify_signature::verify(
278 &self.chain,
279 &self.sender,
280 signature,
281 self.message_type,
282 &self.item_hash,
283 )
284 }
285}
286
287impl From<Message> for MessageHeader {
288 fn from(message: Message) -> Self {
289 MessageHeader {
290 chain: message.chain,
291 sender: message.sender,
292 signature: message.signature,
293 content_source: message.content_source,
294 item_hash: message.item_hash,
295 confirmations: message.confirmations,
296 time: message.time,
297 channel: message.channel,
298 message_type: message.message_type,
299 }
300 }
301}
302
303#[derive(PartialEq, Debug, Clone)]
304pub struct Message {
305 pub chain: Chain,
307 pub sender: Address,
309 pub signature: Option<Signature>,
315 pub content_source: ContentSource,
318 pub item_hash: ItemHash,
320 pub confirmations: Vec<MessageConfirmation>,
322 pub time: Timestamp,
324 pub channel: Option<Channel>,
326 pub message_type: MessageType,
328 pub content: MessageContent,
330}
331
332impl Message {
333 pub fn content(&self) -> &MessageContentEnum {
334 &self.content.content
335 }
336
337 pub fn confirmed(&self) -> bool {
338 !self.confirmations.is_empty()
339 }
340
341 pub fn sender(&self) -> &Address {
345 &self.sender
346 }
347
348 pub fn owner(&self) -> &Address {
350 &self.content.address
351 }
352
353 pub fn sent_at(&self) -> &Timestamp {
358 &self.content.time
359 }
360
361 pub fn confirmed_at(&self) -> Option<&Timestamp> {
363 self.confirmations.first().and_then(|c| c.time.as_ref())
364 }
365
366 pub fn verify_item_hash(&self) -> Result<(), MessageVerificationError> {
372 match self.content_source.verify_inline_hash(&self.item_hash) {
373 Some(Ok(())) => Ok(()),
374 Some(Err((expected, actual))) => {
375 Err(MessageVerificationError::ItemHashVerificationFailed { expected, actual })
376 }
377 None => Err(MessageVerificationError::NonInlineMessage),
378 }
379 }
380
381 #[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
388 pub fn verify_signature(
389 &self,
390 ) -> Result<(), crate::verify_signature::SignatureVerificationError> {
391 let signature = self
392 .signature
393 .as_ref()
394 .ok_or(crate::verify_signature::SignatureVerificationError::MissingSignature)?;
395 crate::verify_signature::verify(
396 &self.chain,
397 &self.sender,
398 signature,
399 self.message_type,
400 &self.item_hash,
401 )
402 }
403}
404
405#[derive(Deserialize)]
408struct MessageHeaderRaw {
409 chain: Chain,
410 sender: Address,
411 signature: Option<Signature>,
412 #[serde(flatten)]
413 content_source: ContentSource,
414 item_hash: ItemHash,
415 #[serde(default)]
416 confirmations: Option<Vec<MessageConfirmation>>,
417 time: Timestamp,
418 #[serde(default)]
419 channel: Option<Channel>,
420 #[serde(rename = "type")]
421 message_type: MessageType,
422}
423
424impl MessageHeaderRaw {
425 fn into_header(self) -> MessageHeader {
426 MessageHeader {
427 chain: self.chain,
428 sender: self.sender,
429 signature: self.signature,
430 content_source: self.content_source,
431 item_hash: self.item_hash,
432 confirmations: self.confirmations.unwrap_or_default(),
433 time: self.time,
434 channel: self.channel,
435 message_type: self.message_type,
436 }
437 }
438}
439
440impl<'de> Deserialize<'de> for MessageHeader {
441 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
442 where
443 D: Deserializer<'de>,
444 {
445 MessageHeaderRaw::deserialize(deserializer).map(MessageHeaderRaw::into_header)
446 }
447}
448
449impl<'de> Deserialize<'de> for Message {
451 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
452 where
453 D: Deserializer<'de>,
454 {
455 #[derive(Deserialize)]
456 struct MessageRaw {
457 #[serde(flatten)]
458 header: MessageHeaderRaw,
459 content: serde_json::Value,
460 }
461
462 let raw = MessageRaw::deserialize(deserializer)?;
463
464 let content = MessageContent::from_json_value(raw.header.message_type, &raw.content)
465 .map_err(de::Error::custom)?;
466
467 Ok(raw.header.into_header().with_content(content))
468 }
469}
470
471impl Serialize for Message {
473 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
474 where
475 S: serde::Serializer,
476 {
477 use serde::ser::SerializeStruct;
478
479 let mut state = serializer.serialize_struct("Message", 9)?;
480 state.serialize_field("chain", &self.chain)?;
481 state.serialize_field("sender", &self.sender)?;
482 match &self.content_source {
483 ContentSource::Inline { item_content } => {
484 state.serialize_field("item_type", "inline")?;
485 state.serialize_field("item_content", item_content)?;
486 }
487 ContentSource::Storage => {
488 state.serialize_field("item_type", "storage")?;
489 state.serialize_field("item_content", &None::<String>)?;
490 }
491 ContentSource::Ipfs => {
492 state.serialize_field("item_type", "ipfs")?;
493 state.serialize_field("item_content", &None::<String>)?;
494 }
495 }
496 state.serialize_field("signature", &self.signature)?;
497 state.serialize_field("item_hash", &self.item_hash)?;
498 if self.confirmed() {
499 state.serialize_field("confirmed", &true)?;
500 state.serialize_field("confirmations", &self.confirmations)?;
501 }
502 state.serialize_field("time", &self.time)?;
503 if self.channel.is_some() {
504 state.serialize_field("channel", &self.channel)?;
505 }
506 state.serialize_field("type", &self.message_type)?;
507 state.serialize_field("content", &self.content)?;
508 state.end()
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use crate::item_hash;
516 use assert_matches::assert_matches;
517
518 #[test]
519 fn test_deserialize_item_type_inline() {
520 let item_content_str = "test".to_string();
521 let content_source_str =
522 format!("{{\"item_type\":\"inline\",\"item_content\":\"{item_content_str}\"}}");
523 let content_source: ContentSource = serde_json::from_str(&content_source_str).unwrap();
524
525 assert_matches!(
526 content_source,
527 ContentSource::Inline {
528 item_content
529 } if item_content == item_content_str
530 );
531 }
532
533 #[test]
534 fn test_deserialize_item_type_storage() {
535 let content_source_str = r#"{"item_type":"storage"}"#;
536 let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
537 assert_matches!(content_source, ContentSource::Storage);
538 }
539
540 #[test]
541 fn test_deserialize_item_type_ipfs() {
542 let content_source_str = r#"{"item_type":"ipfs"}"#;
543 let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
544 assert_matches!(content_source, ContentSource::Ipfs);
545 }
546
547 #[test]
548 fn test_verify_inline_message_item_hash() {
549 let json = include_str!("../../../../fixtures/messages/post/post.json");
550 let message: Message = serde_json::from_str(json).unwrap();
551 message.verify_item_hash().unwrap();
552
553 let json = include_str!("../../../../fixtures/messages/store/store-ipfs.json");
555 let message: Message = serde_json::from_str(json).unwrap();
556 message.verify_item_hash().unwrap();
557 }
558
559 #[test]
560 fn test_verify_inline_message_detects_tampered_hash() {
561 let json = include_str!("../../../../fixtures/messages/post/post.json");
562 let mut message: Message = serde_json::from_str(json).unwrap();
563 message.item_hash =
564 item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
565 assert_matches!(
566 message.verify_item_hash(),
567 Err(MessageVerificationError::ItemHashVerificationFailed { .. })
568 );
569 }
570
571 #[test]
572 fn test_verify_inline_message_detects_tampered_content() {
573 let json = include_str!("../../../../fixtures/messages/post/post.json");
574 let mut message: Message = serde_json::from_str(json).unwrap();
575 if let ContentSource::Inline {
577 ref mut item_content,
578 } = message.content_source
579 {
580 item_content.push('!');
581 }
582 assert_matches!(
583 message.verify_item_hash(),
584 Err(MessageVerificationError::ItemHashVerificationFailed { .. })
585 );
586 }
587
588 #[test]
589 fn test_verify_non_inline_message_returns_error() {
590 let json = include_str!("../../../../fixtures/messages/aggregate/aggregate.json");
591 let message: Message = serde_json::from_str(json).unwrap();
592 assert_matches!(
593 message.verify_item_hash(),
594 Err(MessageVerificationError::NonInlineMessage)
595 );
596 }
597
598 #[test]
599 fn test_deserialize_message_header() {
600 let json = include_str!("../../../../fixtures/messages/post/post.json");
601 let header: MessageHeader = serde_json::from_str(json).unwrap();
602 let message: Message = serde_json::from_str(json).unwrap();
603
604 assert_eq!(header.chain, message.chain);
606 assert_eq!(header.sender, message.sender);
607 assert_eq!(header.signature, message.signature);
608 assert_eq!(header.content_source, message.content_source);
609 assert_eq!(header.item_hash, message.item_hash);
610 assert_eq!(header.time, message.time);
611 assert_eq!(header.channel, message.channel);
612 assert_eq!(header.message_type, message.message_type);
613 }
614
615 #[test]
616 fn test_message_header_with_content_roundtrip() {
617 let json = include_str!("../../../../fixtures/messages/post/post.json");
618 let message: Message = serde_json::from_str(json).unwrap();
619
620 let content = message.content.clone();
622 let header = MessageHeader::from(message.clone());
623 let reassembled = header.with_content(content);
624 assert_eq!(reassembled, message);
625 }
626
627 #[test]
631 fn test_deserialize_message_null_signature() {
632 let json = r#"{
633 "chain": "ETH",
634 "sender": "0x34924EF945b931d1E929375556f69CE2E7FE5dcc",
635 "signature": null,
636 "item_type": "storage",
637 "item_hash": "96c3e190177c0f372c24b4b19df032072b2de592224da220a940e980d86f4ce1",
638 "time": 1679321315.0,
639 "type": "STORE",
640 "content": {
641 "address": "0x34924EF945b931d1E929375556f69CE2E7FE5dcc",
642 "time": 1679321315.0,
643 "item_type": "storage",
644 "item_hash": "96c3e190177c0f372c24b4b19df032072b2de592224da220a940e980d86f4ce1"
645 }
646 }"#;
647
648 let message: Message = serde_json::from_str(json).unwrap();
649 assert!(message.signature.is_none());
650
651 let header: MessageHeader = serde_json::from_str(json).unwrap();
652 assert!(header.signature.is_none());
653 }
654
655 #[test]
656 fn test_deserialize_content_with_type() {
657 let json = include_str!("../../../../fixtures/messages/post/post.json");
658 let message: Message = serde_json::from_str(json).unwrap();
659
660 if let ContentSource::Inline { ref item_content } = message.content_source {
662 let content = MessageContent::deserialize_with_type(
663 message.message_type,
664 item_content.as_bytes(),
665 )
666 .unwrap();
667 assert_eq!(content, message.content);
668 } else {
669 panic!("Expected inline message");
670 }
671 }
672
673 #[test]
674 fn test_deserialize_item_type_invalid_type() {
675 let content_source_str = r#"{"item_type":"invalid"}"#;
676 let result = serde_json::from_str::<ContentSource>(content_source_str);
677 assert!(result.is_err());
678 }
679
680 #[test]
681 fn test_deserialize_item_type_invalid_format() {
682 let content_source_str = r#"{"type":"inline"}"#;
683 let result = serde_json::from_str::<ContentSource>(content_source_str);
684 assert!(result.is_err());
685 }
686
687 #[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
688 mod signature_tests {
689 use super::*;
690 use crate::verify_signature::SignatureVerificationError;
691
692 #[test]
693 fn test_verify_signature_unsupported_chain() {
694 let json = include_str!("../../../../fixtures/messages/post/post.json");
695 let mut message: Message = serde_json::from_str(json).unwrap();
696 message.chain = Chain::Tezos;
697 assert_matches!(
698 message.verify_signature(),
699 Err(SignatureVerificationError::UnsupportedChain(_))
700 );
701 }
702
703 #[cfg(feature = "signature-evm")]
704 mod evm {
705 use super::*;
706 use crate::chain::Signature;
707
708 fn post_message() -> Message {
709 let json = include_str!("../../../../fixtures/messages/post/post.json");
710 serde_json::from_str(json).unwrap()
711 }
712
713 #[test]
714 fn test_verify_signature_valid() {
715 let message = post_message();
716 message.verify_signature().unwrap();
717 }
718
719 #[test]
720 fn test_verify_signature_tampered_sender() {
721 let mut message = post_message();
722 message.sender =
723 Address::from("0x0000000000000000000000000000000000000000".to_string());
724 assert_matches!(
725 message.verify_signature(),
726 Err(SignatureVerificationError::SignatureMismatch { .. })
727 );
728 }
729
730 #[test]
731 fn test_verify_signature_tampered_item_hash() {
732 let mut message = post_message();
733 message.item_hash =
734 item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
735 assert_matches!(
736 message.verify_signature(),
737 Err(SignatureVerificationError::SignatureMismatch { .. })
738 );
739 }
740
741 #[test]
742 fn test_verify_signature_invalid_hex() {
743 let mut message = post_message();
744 message.signature = Some(Signature::from("not-a-hex-string".to_string()));
745 assert_matches!(
746 message.verify_signature(),
747 Err(SignatureVerificationError::InvalidSignature(_))
748 );
749 }
750
751 #[test]
752 fn test_verify_signature_wrong_but_valid_signature() {
753 let mut message = post_message();
754 message.signature = Some(Signature::from(
755 "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001\
756 00"
757 .to_string(),
758 ));
759 assert_matches!(
761 message.verify_signature(),
762 Err(SignatureVerificationError::SignatureMismatch { .. }
763 | SignatureVerificationError::InvalidSignature(_))
764 );
765 }
766
767 #[test]
768 fn test_verify_signature_evm_chain_dispatch() {
769 let mut message = post_message();
770 message.chain = Chain::Arbitrum;
771 assert_matches!(
774 message.verify_signature(),
775 Err(SignatureVerificationError::SignatureMismatch { .. })
776 );
777 }
778 }
779
780 #[cfg(feature = "signature-sol")]
781 mod sol {
782 use super::*;
783
784 fn sol_post_message() -> Message {
785 let json = include_str!("../../../../fixtures/messages/post/post-sol.json");
786 serde_json::from_str(json).unwrap()
787 }
788
789 #[test]
790 fn test_verify_sol_signature_valid() {
791 let message = sol_post_message();
792 message.verify_signature().unwrap();
793 }
794
795 #[test]
796 fn test_verify_sol_signature_tampered_sender() {
797 let mut message = sol_post_message();
798 message.sender = Address::from("11111111111111111111111111111111".to_string());
799 assert_matches!(
800 message.verify_signature(),
801 Err(SignatureVerificationError::InvalidSignature(_))
802 );
803 }
804
805 #[test]
806 fn test_verify_sol_signature_tampered_item_hash() {
807 let mut message = sol_post_message();
808 message.item_hash =
809 item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
810 assert_matches!(
811 message.verify_signature(),
812 Err(SignatureVerificationError::InvalidSignature(_))
813 );
814 }
815
816 #[test]
817 fn test_deserialize_sol_signature_format() {
818 let message = sol_post_message();
819 let sig = message.signature.as_ref().expect("sol fixture is signed");
820 assert!(sig.public_key().is_some());
821 assert_eq!(
822 sig.public_key().unwrap(),
823 "5SwCeHbZ9oY3556YFBEhPTHyy9t4yse26v7MUyGm2bHS"
824 );
825 }
826 }
827 }
828}