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