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, 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/// 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    ///
228    /// `None` for legacy pre-enforcement-era mainnet messages that pyaleph
229    /// accepted unsigned and still serves with `signature: null`. Such
230    /// messages cannot be re-posted or signature-verified.
231    pub signature: Option<Signature>,
232    /// Content of the message as created by the sender. Can either be inline or stored
233    /// on Aleph Cloud.
234    pub content_source: ContentSource,
235    /// Hash of the content (SHA2-256).
236    pub item_hash: ItemHash,
237    /// List of confirmations for the message.
238    pub confirmations: Vec<MessageConfirmation>,
239    /// Unix timestamp or datetime when the message was published.
240    pub time: Timestamp,
241    /// Channel of the message, one application ideally has one channel.
242    pub channel: Option<Channel>,
243    /// Message type. (aggregate, forget, instance, post, program, store).
244    pub message_type: MessageType,
245}
246
247impl MessageHeader {
248    /// Assembles a full [`Message`] by combining this header with deserialized content.
249    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    /// Verifies that the message signature was produced by the sender.
265    ///
266    /// Signature verification only depends on header fields (chain, sender,
267    /// signature, message_type, item_hash), so it can run before content is
268    /// downloaded or deserialized.
269    #[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    /// Blockchain used for this message.
306    pub chain: Chain,
307    /// Sender address.
308    pub sender: Address,
309    /// Cryptographic signature of the message by the sender.
310    ///
311    /// `None` for legacy pre-enforcement-era mainnet messages that pyaleph
312    /// accepted unsigned and still serves with `signature: null`. Such
313    /// messages cannot be re-posted or signature-verified.
314    pub signature: Option<Signature>,
315    /// Content of the message as created by the sender. Can either be inline or stored
316    /// on Aleph Cloud.
317    pub content_source: ContentSource,
318    /// Hash of the content (SHA2-256).
319    pub item_hash: ItemHash,
320    /// List of confirmations for the message.
321    pub confirmations: Vec<MessageConfirmation>,
322    /// Unix timestamp or datetime when the message was published.
323    pub time: Timestamp,
324    /// Channel of the message, one application ideally has one channel.
325    pub channel: Option<Channel>,
326    /// Message type. (aggregate, forget, instance, post, program, store).
327    pub message_type: MessageType,
328    /// Message content.
329    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    /// Returns the address of the sender of the message. Note that the sender is not necessarily
342    /// the owner of the resources, as the owner may have delegated their authority to create
343    /// specific resources through the permission system.
344    pub fn sender(&self) -> &Address {
345        &self.sender
346    }
347
348    /// Returns the address of the owner of the resources.
349    pub fn owner(&self) -> &Address {
350        &self.content.address
351    }
352
353    /// Returns the time at which the message was sent.
354    /// Notes:
355    /// * This value is signed by the sender and should not be trusted accordingly.
356    /// * We prefer `content.time` over `time` as `time` is not part of the signed payload.
357    pub fn sent_at(&self) -> &Timestamp {
358        &self.content.time
359    }
360
361    /// Returns the earliest confirmation time of the message.
362    pub fn confirmed_at(&self) -> Option<&Timestamp> {
363        self.confirmations.first().and_then(|c| c.time.as_ref())
364    }
365
366    /// Verifies that the item hash of an inline message matches its content.
367    ///
368    /// For inline messages, the item hash is the SHA-256 hash of the `item_content` string.
369    /// For non-inline messages (storage/ipfs), use the client's `verify_message()` method
370    /// instead, which downloads the raw content from `/storage/raw/` for verification.
371    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    /// Verifies that the message signature was produced by the sender.
382    ///
383    /// Constructs the verification buffer from the message fields, then
384    /// dispatches to the chain-specific verification algorithm. Currently
385    /// supports EVM-compatible chains (Ethereum, Arbitrum, etc.) and SVM
386    /// chains (Solana, Eclipse).
387    #[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/// Shared helper struct for deserializing message header fields.
406/// Used by both `Message` and `MessageHeader` Deserialize impls.
407#[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
449// Custom deserializer that uses message_type to efficiently deserialize content
450impl<'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
471// Manual Serialize for Message
472impl 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        // STORE message envelope is inline; only the referenced file lives on IPFS.
554        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        // Corrupt the item_content while keeping the original item_hash
576        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        // Header fields should match Message fields
605        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        // Convert to header, then reassemble with original content
621        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    /// Pyaleph serves a small number of legacy mainnet messages (pre-signature
628    /// enforcement) with `signature: null`. They must deserialize successfully
629    /// so that listing endpoints can return whole pages without erroring out.
630    #[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        // For inline messages, deserialize_with_type from item_content should match
661        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                // May recover to a different address or fail to recover entirely
760                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                // Buffer now contains "ARB" instead of "ETH", so signature won't match,
772                // but the dispatch should route to the Ethereum verifier (not UnsupportedChain).
773                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}