Skip to main content

aleph_types/verify_signature/
mod.rs

1#[cfg(feature = "signature-evm")]
2pub(crate) mod ethereum;
3#[cfg(feature = "signature-sol")]
4mod solana;
5
6use crate::chain::{Address, Chain, Signature};
7use crate::item_hash::ItemHash;
8use crate::message::MessageType;
9use thiserror::Error;
10
11#[derive(Error, Debug)]
12#[non_exhaustive]
13pub enum SignatureVerificationError {
14    /// The recovered signer address doesn't match the message sender.
15    #[error(
16        "Signature mismatch: message sender is {expected}, but signature was produced by {recovered}"
17    )]
18    SignatureMismatch {
19        expected: Address,
20        recovered: Address,
21    },
22    /// The signature bytes could not be parsed or recovery failed.
23    #[error("Invalid signature: {0}")]
24    InvalidSignature(String),
25    /// Signature verification is not implemented for this chain.
26    #[error("Unsupported chain for signature verification: {0}")]
27    UnsupportedChain(Chain),
28    /// The message has no signature attached. Some legacy pyaleph mainnet
29    /// messages were accepted without a signature and are served with
30    /// `signature: null`; nothing can be verified for them.
31    #[error("Message has no signature to verify")]
32    MissingSignature,
33}
34
35/// Constructs the verification buffer that was signed by the sender.
36/// Format: "{chain}\n{sender}\n{message_type}\n{item_hash}"
37fn verification_buffer(
38    chain: &Chain,
39    sender: &Address,
40    message_type: MessageType,
41    item_hash: &ItemHash,
42) -> String {
43    format!("{chain}\n{sender}\n{message_type}\n{item_hash}")
44}
45
46/// Verifies the cryptographic signature of a message.
47pub fn verify(
48    chain: &Chain,
49    sender: &Address,
50    signature: &Signature,
51    message_type: MessageType,
52    item_hash: &ItemHash,
53) -> Result<(), SignatureVerificationError> {
54    let buffer = verification_buffer(chain, sender, message_type, item_hash);
55
56    #[cfg(feature = "signature-evm")]
57    if chain.is_evm() {
58        let recovered = ethereum::recover_address(buffer.as_bytes(), signature.as_str())?;
59        let recovered_addr = Address::from(recovered);
60
61        if !sender
62            .as_str()
63            .eq_ignore_ascii_case(recovered_addr.as_str())
64        {
65            return Err(SignatureVerificationError::SignatureMismatch {
66                expected: sender.clone(),
67                recovered: recovered_addr,
68            });
69        }
70
71        return Ok(());
72    }
73
74    #[cfg(feature = "signature-sol")]
75    if chain.is_svm() {
76        // For SVM chains, the sender address is the base58-encoded Ed25519 public key.
77        return solana::verify(buffer.as_bytes(), signature.as_str(), sender.as_str());
78    }
79
80    Err(SignatureVerificationError::UnsupportedChain(chain.clone()))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::{address, item_hash};
87
88    #[test]
89    fn test_verification_buffer_format() {
90        let chain = Chain::Ethereum;
91        let sender = address!("0xB68B9D4f3771c246233823ed1D3Add451055F9Ef");
92        let message_type = MessageType::Post;
93        let item_hash =
94            item_hash!("d281eb8a69ba1f4dda2d71aaf3ded06caa92edd690ef3d0632f41aa91167762c");
95
96        let buffer = verification_buffer(&chain, &sender, message_type, &item_hash);
97
98        assert_eq!(
99            buffer,
100            "ETH\n\
101             0xB68B9D4f3771c246233823ed1D3Add451055F9Ef\n\
102             POST\n\
103             d281eb8a69ba1f4dda2d71aaf3ded06caa92edd690ef3d0632f41aa91167762c"
104        );
105    }
106
107    #[test]
108    fn test_verification_buffer_different_chain_and_type() {
109        let chain = Chain::Arbitrum;
110        let sender = address!("0xABCD");
111        let message_type = MessageType::Aggregate;
112        let item_hash =
113            item_hash!("0000000000000000000000000000000000000000000000000000000000000001");
114
115        let buffer = verification_buffer(&chain, &sender, message_type, &item_hash);
116
117        assert_eq!(
118            buffer,
119            "ARB\n0xABCD\nAGGREGATE\n0000000000000000000000000000000000000000000000000000000000000001"
120        );
121    }
122
123    #[cfg(feature = "signature-evm")]
124    #[test]
125    fn test_verify_with_v_zero_format() {
126        // The fixture signature ends with 1b (v=27). Replacing the last byte
127        // with 00 (v=0) should be equivalent for recovery.
128        let json = include_str!("../../../../fixtures/messages/post/post.json");
129        let mut message: crate::message::Message = serde_json::from_str(json).unwrap();
130
131        // Original signature ends with "1b" (v=27); replace with "00" (v=0)
132        let sig = message.signature.as_ref().unwrap().as_str().to_string();
133        let normalized_sig = format!("{}00", &sig[..sig.len() - 2]);
134        message.signature = Some(Signature::from(normalized_sig));
135
136        message.verify_signature().unwrap();
137    }
138}