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, 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: Signature,
228 pub content_source: ContentSource,
231 pub item_hash: ItemHash,
233 pub confirmations: Vec<MessageConfirmation>,
235 pub time: Timestamp,
237 pub channel: Option<Channel>,
239 pub message_type: MessageType,
241}
242
243impl MessageHeader {
244 pub fn with_content(self, content: MessageContent) -> Message {
246 Message {
247 chain: self.chain,
248 sender: self.sender,
249 signature: self.signature,
250 content_source: self.content_source,
251 item_hash: self.item_hash,
252 confirmations: self.confirmations,
253 time: self.time,
254 channel: self.channel,
255 message_type: self.message_type,
256 content,
257 }
258 }
259
260 #[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
266 pub fn verify_signature(
267 &self,
268 ) -> Result<(), crate::verify_signature::SignatureVerificationError> {
269 crate::verify_signature::verify(
270 &self.chain,
271 &self.sender,
272 &self.signature,
273 self.message_type,
274 &self.item_hash,
275 )
276 }
277}
278
279impl From<Message> for MessageHeader {
280 fn from(message: Message) -> Self {
281 MessageHeader {
282 chain: message.chain,
283 sender: message.sender,
284 signature: message.signature,
285 content_source: message.content_source,
286 item_hash: message.item_hash,
287 confirmations: message.confirmations,
288 time: message.time,
289 channel: message.channel,
290 message_type: message.message_type,
291 }
292 }
293}
294
295#[derive(PartialEq, Debug, Clone)]
296pub struct Message {
297 pub chain: Chain,
299 pub sender: Address,
301 pub signature: Signature,
303 pub content_source: ContentSource,
306 pub item_hash: ItemHash,
308 pub confirmations: Vec<MessageConfirmation>,
310 pub time: Timestamp,
312 pub channel: Option<Channel>,
314 pub message_type: MessageType,
316 pub content: MessageContent,
318}
319
320impl Message {
321 pub fn content(&self) -> &MessageContentEnum {
322 &self.content.content
323 }
324
325 pub fn confirmed(&self) -> bool {
326 !self.confirmations.is_empty()
327 }
328
329 pub fn sender(&self) -> &Address {
333 &self.sender
334 }
335
336 pub fn owner(&self) -> &Address {
338 &self.content.address
339 }
340
341 pub fn sent_at(&self) -> &Timestamp {
346 &self.content.time
347 }
348
349 pub fn confirmed_at(&self) -> Option<&Timestamp> {
351 self.confirmations.first().and_then(|c| c.time.as_ref())
352 }
353
354 pub fn verify_item_hash(&self) -> Result<(), MessageVerificationError> {
360 match self.content_source.verify_inline_hash(&self.item_hash) {
361 Some(Ok(())) => Ok(()),
362 Some(Err((expected, actual))) => {
363 Err(MessageVerificationError::ItemHashVerificationFailed { expected, actual })
364 }
365 None => Err(MessageVerificationError::NonInlineMessage),
366 }
367 }
368
369 #[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
376 pub fn verify_signature(
377 &self,
378 ) -> Result<(), crate::verify_signature::SignatureVerificationError> {
379 crate::verify_signature::verify(
380 &self.chain,
381 &self.sender,
382 &self.signature,
383 self.message_type,
384 &self.item_hash,
385 )
386 }
387}
388
389#[derive(Deserialize)]
392struct MessageHeaderRaw {
393 chain: Chain,
394 sender: Address,
395 signature: Signature,
396 #[serde(flatten)]
397 content_source: ContentSource,
398 item_hash: ItemHash,
399 #[serde(default)]
400 confirmations: Option<Vec<MessageConfirmation>>,
401 time: Timestamp,
402 #[serde(default)]
403 channel: Option<Channel>,
404 #[serde(rename = "type")]
405 message_type: MessageType,
406}
407
408impl MessageHeaderRaw {
409 fn into_header(self) -> MessageHeader {
410 MessageHeader {
411 chain: self.chain,
412 sender: self.sender,
413 signature: self.signature,
414 content_source: self.content_source,
415 item_hash: self.item_hash,
416 confirmations: self.confirmations.unwrap_or_default(),
417 time: self.time,
418 channel: self.channel,
419 message_type: self.message_type,
420 }
421 }
422}
423
424impl<'de> Deserialize<'de> for MessageHeader {
425 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
426 where
427 D: Deserializer<'de>,
428 {
429 MessageHeaderRaw::deserialize(deserializer).map(MessageHeaderRaw::into_header)
430 }
431}
432
433impl<'de> Deserialize<'de> for Message {
435 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
436 where
437 D: Deserializer<'de>,
438 {
439 #[derive(Deserialize)]
440 struct MessageRaw {
441 #[serde(flatten)]
442 header: MessageHeaderRaw,
443 content: serde_json::Value,
444 }
445
446 let raw = MessageRaw::deserialize(deserializer)?;
447
448 let content = MessageContent::from_json_value(raw.header.message_type, &raw.content)
449 .map_err(de::Error::custom)?;
450
451 Ok(raw.header.into_header().with_content(content))
452 }
453}
454
455impl Serialize for Message {
457 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
458 where
459 S: serde::Serializer,
460 {
461 use serde::ser::SerializeStruct;
462
463 let mut state = serializer.serialize_struct("Message", 9)?;
464 state.serialize_field("chain", &self.chain)?;
465 state.serialize_field("sender", &self.sender)?;
466 match &self.content_source {
467 ContentSource::Inline { item_content } => {
468 state.serialize_field("item_type", "inline")?;
469 state.serialize_field("item_content", item_content)?;
470 }
471 ContentSource::Storage => {
472 state.serialize_field("item_type", "storage")?;
473 state.serialize_field("item_content", &None::<String>)?;
474 }
475 ContentSource::Ipfs => {
476 state.serialize_field("item_type", "ipfs")?;
477 state.serialize_field("item_content", &None::<String>)?;
478 }
479 }
480 state.serialize_field("signature", &self.signature)?;
481 state.serialize_field("item_hash", &self.item_hash)?;
482 if self.confirmed() {
483 state.serialize_field("confirmed", &true)?;
484 state.serialize_field("confirmations", &self.confirmations)?;
485 }
486 state.serialize_field("time", &self.time)?;
487 if self.channel.is_some() {
488 state.serialize_field("channel", &self.channel)?;
489 }
490 state.serialize_field("type", &self.message_type)?;
491 state.serialize_field("content", &self.content)?;
492 state.end()
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use crate::item_hash;
500 use assert_matches::assert_matches;
501
502 #[test]
503 fn test_deserialize_item_type_inline() {
504 let item_content_str = "test".to_string();
505 let content_source_str =
506 format!("{{\"item_type\":\"inline\",\"item_content\":\"{item_content_str}\"}}");
507 let content_source: ContentSource = serde_json::from_str(&content_source_str).unwrap();
508
509 assert_matches!(
510 content_source,
511 ContentSource::Inline {
512 item_content
513 } if item_content == item_content_str
514 );
515 }
516
517 #[test]
518 fn test_deserialize_item_type_storage() {
519 let content_source_str = r#"{"item_type":"storage"}"#;
520 let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
521 assert_matches!(content_source, ContentSource::Storage);
522 }
523
524 #[test]
525 fn test_deserialize_item_type_ipfs() {
526 let content_source_str = r#"{"item_type":"ipfs"}"#;
527 let content_source: ContentSource = serde_json::from_str(content_source_str).unwrap();
528 assert_matches!(content_source, ContentSource::Ipfs);
529 }
530
531 #[test]
532 fn test_verify_inline_message_item_hash() {
533 let json = include_str!("../../../../fixtures/messages/post/post.json");
534 let message: Message = serde_json::from_str(json).unwrap();
535 message.verify_item_hash().unwrap();
536
537 let json = include_str!("../../../../fixtures/messages/store/store-ipfs.json");
539 let message: Message = serde_json::from_str(json).unwrap();
540 message.verify_item_hash().unwrap();
541 }
542
543 #[test]
544 fn test_verify_inline_message_detects_tampered_hash() {
545 let json = include_str!("../../../../fixtures/messages/post/post.json");
546 let mut message: Message = serde_json::from_str(json).unwrap();
547 message.item_hash =
548 item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
549 assert_matches!(
550 message.verify_item_hash(),
551 Err(MessageVerificationError::ItemHashVerificationFailed { .. })
552 );
553 }
554
555 #[test]
556 fn test_verify_inline_message_detects_tampered_content() {
557 let json = include_str!("../../../../fixtures/messages/post/post.json");
558 let mut message: Message = serde_json::from_str(json).unwrap();
559 if let ContentSource::Inline {
561 ref mut item_content,
562 } = message.content_source
563 {
564 item_content.push('!');
565 }
566 assert_matches!(
567 message.verify_item_hash(),
568 Err(MessageVerificationError::ItemHashVerificationFailed { .. })
569 );
570 }
571
572 #[test]
573 fn test_verify_non_inline_message_returns_error() {
574 let json = include_str!("../../../../fixtures/messages/aggregate/aggregate.json");
575 let message: Message = serde_json::from_str(json).unwrap();
576 assert_matches!(
577 message.verify_item_hash(),
578 Err(MessageVerificationError::NonInlineMessage)
579 );
580 }
581
582 #[test]
583 fn test_deserialize_message_header() {
584 let json = include_str!("../../../../fixtures/messages/post/post.json");
585 let header: MessageHeader = serde_json::from_str(json).unwrap();
586 let message: Message = serde_json::from_str(json).unwrap();
587
588 assert_eq!(header.chain, message.chain);
590 assert_eq!(header.sender, message.sender);
591 assert_eq!(header.signature, message.signature);
592 assert_eq!(header.content_source, message.content_source);
593 assert_eq!(header.item_hash, message.item_hash);
594 assert_eq!(header.time, message.time);
595 assert_eq!(header.channel, message.channel);
596 assert_eq!(header.message_type, message.message_type);
597 }
598
599 #[test]
600 fn test_message_header_with_content_roundtrip() {
601 let json = include_str!("../../../../fixtures/messages/post/post.json");
602 let message: Message = serde_json::from_str(json).unwrap();
603
604 let content = message.content.clone();
606 let header = MessageHeader::from(message.clone());
607 let reassembled = header.with_content(content);
608 assert_eq!(reassembled, message);
609 }
610
611 #[test]
612 fn test_deserialize_content_with_type() {
613 let json = include_str!("../../../../fixtures/messages/post/post.json");
614 let message: Message = serde_json::from_str(json).unwrap();
615
616 if let ContentSource::Inline { ref item_content } = message.content_source {
618 let content = MessageContent::deserialize_with_type(
619 message.message_type,
620 item_content.as_bytes(),
621 )
622 .unwrap();
623 assert_eq!(content, message.content);
624 } else {
625 panic!("Expected inline message");
626 }
627 }
628
629 #[test]
630 fn test_deserialize_item_type_invalid_type() {
631 let content_source_str = r#"{"item_type":"invalid"}"#;
632 let result = serde_json::from_str::<ContentSource>(content_source_str);
633 assert!(result.is_err());
634 }
635
636 #[test]
637 fn test_deserialize_item_type_invalid_format() {
638 let content_source_str = r#"{"type":"inline"}"#;
639 let result = serde_json::from_str::<ContentSource>(content_source_str);
640 assert!(result.is_err());
641 }
642
643 #[cfg(any(feature = "signature-evm", feature = "signature-sol"))]
644 mod signature_tests {
645 use super::*;
646 use crate::verify_signature::SignatureVerificationError;
647
648 #[test]
649 fn test_verify_signature_unsupported_chain() {
650 let json = include_str!("../../../../fixtures/messages/post/post.json");
651 let mut message: Message = serde_json::from_str(json).unwrap();
652 message.chain = Chain::Tezos;
653 assert_matches!(
654 message.verify_signature(),
655 Err(SignatureVerificationError::UnsupportedChain(_))
656 );
657 }
658
659 #[cfg(feature = "signature-evm")]
660 mod evm {
661 use super::*;
662 use crate::chain::Signature;
663
664 fn post_message() -> Message {
665 let json = include_str!("../../../../fixtures/messages/post/post.json");
666 serde_json::from_str(json).unwrap()
667 }
668
669 #[test]
670 fn test_verify_signature_valid() {
671 let message = post_message();
672 message.verify_signature().unwrap();
673 }
674
675 #[test]
676 fn test_verify_signature_tampered_sender() {
677 let mut message = post_message();
678 message.sender =
679 Address::from("0x0000000000000000000000000000000000000000".to_string());
680 assert_matches!(
681 message.verify_signature(),
682 Err(SignatureVerificationError::SignatureMismatch { .. })
683 );
684 }
685
686 #[test]
687 fn test_verify_signature_tampered_item_hash() {
688 let mut message = post_message();
689 message.item_hash =
690 item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
691 assert_matches!(
692 message.verify_signature(),
693 Err(SignatureVerificationError::SignatureMismatch { .. })
694 );
695 }
696
697 #[test]
698 fn test_verify_signature_invalid_hex() {
699 let mut message = post_message();
700 message.signature = Signature::from("not-a-hex-string".to_string());
701 assert_matches!(
702 message.verify_signature(),
703 Err(SignatureVerificationError::InvalidSignature(_))
704 );
705 }
706
707 #[test]
708 fn test_verify_signature_wrong_but_valid_signature() {
709 let mut message = post_message();
710 message.signature = Signature::from(
711 "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001\
712 00"
713 .to_string(),
714 );
715 assert_matches!(
717 message.verify_signature(),
718 Err(SignatureVerificationError::SignatureMismatch { .. }
719 | SignatureVerificationError::InvalidSignature(_))
720 );
721 }
722
723 #[test]
724 fn test_verify_signature_evm_chain_dispatch() {
725 let mut message = post_message();
726 message.chain = Chain::Arbitrum;
727 assert_matches!(
730 message.verify_signature(),
731 Err(SignatureVerificationError::SignatureMismatch { .. })
732 );
733 }
734 }
735
736 #[cfg(feature = "signature-sol")]
737 mod sol {
738 use super::*;
739
740 fn sol_post_message() -> Message {
741 let json = include_str!("../../../../fixtures/messages/post/post-sol.json");
742 serde_json::from_str(json).unwrap()
743 }
744
745 #[test]
746 fn test_verify_sol_signature_valid() {
747 let message = sol_post_message();
748 message.verify_signature().unwrap();
749 }
750
751 #[test]
752 fn test_verify_sol_signature_tampered_sender() {
753 let mut message = sol_post_message();
754 message.sender = Address::from("11111111111111111111111111111111".to_string());
755 assert_matches!(
756 message.verify_signature(),
757 Err(SignatureVerificationError::InvalidSignature(_))
758 );
759 }
760
761 #[test]
762 fn test_verify_sol_signature_tampered_item_hash() {
763 let mut message = sol_post_message();
764 message.item_hash =
765 item_hash!("0000000000000000000000000000000000000000000000000000000000000000");
766 assert_matches!(
767 message.verify_signature(),
768 Err(SignatureVerificationError::InvalidSignature(_))
769 );
770 }
771
772 #[test]
773 fn test_deserialize_sol_signature_format() {
774 let message = sol_post_message();
775 assert!(message.signature.public_key().is_some());
776 assert_eq!(
777 message.signature.public_key().unwrap(),
778 "5SwCeHbZ9oY3556YFBEhPTHyy9t4yse26v7MUyGm2bHS"
779 );
780 }
781 }
782 }
783}