Skip to main content

aleph_types/message/
base_message.rs

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/// Content variants for different message types.
80#[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    /// Deserializes message content from raw JSON bytes, using `message_type` to select
101    /// the correct content variant. This is the type-directed deserialization used by
102    /// the verified message path.
103    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/// Where to find the content of the message. Note that this is a mix of ItemType / ItemContent
151/// if you are used to the Python SDK.
152#[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    /// For inline messages, verifies that `item_content` hashes to `expected_hash`.
192    ///
193    /// Returns `Some(Ok(()))` if the hash matches, `Some(Err((expected, actual)))` on mismatch,
194    /// or `None` for non-inline messages (which require network-based verification).
195    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/// A message without its deserialized content.
214///
215/// Used by the verified message path: the client fetches message headers, then downloads
216/// and verifies raw content separately before deserializing it. This avoids trusting
217/// the CCN's pre-deserialized `content` field.
218#[derive(PartialEq, Debug, Clone)]
219pub struct MessageHeader {
220    /// Blockchain used for this message.
221    pub chain: Chain,
222    /// Sender address.
223    pub sender: Address,
224    /// Cryptographic signature of the message by the sender.
225    pub signature: Signature,
226    /// Content of the message as created by the sender. Can either be inline or stored
227    /// on Aleph Cloud.
228    pub content_source: ContentSource,
229    /// Hash of the content (SHA2-256).
230    pub item_hash: ItemHash,
231    /// List of confirmations for the message.
232    pub confirmations: Vec<MessageConfirmation>,
233    /// Unix timestamp or datetime when the message was published.
234    pub time: Timestamp,
235    /// Channel of the message, one application ideally has one channel.
236    pub channel: Option<Channel>,
237    /// Message type. (aggregate, forget, instance, post, program, store).
238    pub message_type: MessageType,
239}
240
241impl MessageHeader {
242    /// Assembles a full [`Message`] by combining this header with deserialized content.
243    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    /// Verifies that the message signature was produced by the sender.
259    ///
260    /// Signature verification only depends on header fields (chain, sender,
261    /// signature, message_type, item_hash), so it can run before content is
262    /// downloaded or deserialized.
263    #[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    /// Blockchain used for this message.
296    pub chain: Chain,
297    /// Sender address.
298    pub sender: Address,
299    /// Cryptographic signature of the message by the sender.
300    pub signature: Signature,
301    /// Content of the message as created by the sender. Can either be inline or stored
302    /// on Aleph Cloud.
303    pub content_source: ContentSource,
304    /// Hash of the content (SHA2-256).
305    pub item_hash: ItemHash,
306    /// List of confirmations for the message.
307    pub confirmations: Vec<MessageConfirmation>,
308    /// Unix timestamp or datetime when the message was published.
309    pub time: Timestamp,
310    /// Channel of the message, one application ideally has one channel.
311    pub channel: Option<Channel>,
312    /// Message type. (aggregate, forget, instance, post, program, store).
313    pub message_type: MessageType,
314    /// Message content.
315    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    /// Returns the address of the sender of the message. Note that the sender is not necessarily
328    /// the owner of the resources, as the owner may have delegated their authority to create
329    /// specific resources through the permission system.
330    pub fn sender(&self) -> &Address {
331        &self.sender
332    }
333
334    /// Returns the address of the owner of the resources.
335    pub fn owner(&self) -> &Address {
336        &self.content.address
337    }
338
339    /// Returns the time at which the message was sent.
340    /// Notes:
341    /// * This value is signed by the sender and should not be trusted accordingly.
342    /// * We prefer `content.time` over `time` as `time` is not part of the signed payload.
343    pub fn sent_at(&self) -> &Timestamp {
344        &self.content.time
345    }
346
347    /// Returns the earliest confirmation time of the message.
348    pub fn confirmed_at(&self) -> Option<&Timestamp> {
349        self.confirmations.first().and_then(|c| c.time.as_ref())
350    }
351
352    /// Verifies that the item hash of an inline message matches its content.
353    ///
354    /// For inline messages, the item hash is the SHA-256 hash of the `item_content` string.
355    /// For non-inline messages (storage/ipfs), use the client's `verify_message()` method
356    /// instead, which downloads the raw content from `/storage/raw/` for verification.
357    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    /// Verifies that the message signature was produced by the sender.
368    ///
369    /// Constructs the verification buffer from the message fields, then
370    /// dispatches to the chain-specific verification algorithm. Currently
371    /// supports EVM-compatible chains (Ethereum, Arbitrum, etc.) and SVM
372    /// chains (Solana, Eclipse).
373    #[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/// Shared helper struct for deserializing message header fields.
388/// Used by both `Message` and `MessageHeader` Deserialize impls.
389#[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
431// Custom deserializer that uses message_type to efficiently deserialize content
432impl<'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
453// Manual Serialize for Message
454impl 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        // STORE message envelope is inline; only the referenced file lives on IPFS.
536        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        // Corrupt the item_content while keeping the original item_hash
558        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        // Header fields should match Message fields
587        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        // Convert to header, then reassemble with original content
603        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        // For inline messages, deserialize_with_type from item_content should match
615        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                // May recover to a different address or fail to recover entirely
714                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                // Buffer now contains "ARB" instead of "ETH", so signature won't match,
726                // but the dispatch should route to the Ethereum verifier (not UnsupportedChain).
727                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}