Skip to main content

aleph_types/verify_signature/
mod.rs

1#[cfg(feature = "signature-evm")]
2mod 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}
29
30/// Constructs the verification buffer that was signed by the sender.
31/// Format: "{chain}\n{sender}\n{message_type}\n{item_hash}"
32fn verification_buffer(
33    chain: &Chain,
34    sender: &Address,
35    message_type: MessageType,
36    item_hash: &ItemHash,
37) -> String {
38    format!("{chain}\n{sender}\n{message_type}\n{item_hash}")
39}
40
41/// Verifies the cryptographic signature of a message.
42pub(crate) fn verify(
43    chain: &Chain,
44    sender: &Address,
45    signature: &Signature,
46    message_type: MessageType,
47    item_hash: &ItemHash,
48) -> Result<(), SignatureVerificationError> {
49    let buffer = verification_buffer(chain, sender, message_type, item_hash);
50
51    #[cfg(feature = "signature-evm")]
52    if chain.is_evm() {
53        let recovered = ethereum::recover_address(buffer.as_bytes(), signature.as_str())?;
54        let recovered_addr = Address::from(recovered);
55
56        if !sender
57            .as_str()
58            .eq_ignore_ascii_case(recovered_addr.as_str())
59        {
60            return Err(SignatureVerificationError::SignatureMismatch {
61                expected: sender.clone(),
62                recovered: recovered_addr,
63            });
64        }
65
66        return Ok(());
67    }
68
69    #[cfg(feature = "signature-sol")]
70    if chain.is_svm() {
71        // For SVM chains, the sender address is the base58-encoded Ed25519 public key.
72        return solana::verify(buffer.as_bytes(), signature.as_str(), sender.as_str());
73    }
74
75    Err(SignatureVerificationError::UnsupportedChain(chain.clone()))
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::{address, item_hash};
82
83    #[test]
84    fn test_verification_buffer_format() {
85        let chain = Chain::Ethereum;
86        let sender = address!("0xB68B9D4f3771c246233823ed1D3Add451055F9Ef");
87        let message_type = MessageType::Post;
88        let item_hash =
89            item_hash!("d281eb8a69ba1f4dda2d71aaf3ded06caa92edd690ef3d0632f41aa91167762c");
90
91        let buffer = verification_buffer(&chain, &sender, message_type, &item_hash);
92
93        assert_eq!(
94            buffer,
95            "ETH\n\
96             0xB68B9D4f3771c246233823ed1D3Add451055F9Ef\n\
97             POST\n\
98             d281eb8a69ba1f4dda2d71aaf3ded06caa92edd690ef3d0632f41aa91167762c"
99        );
100    }
101
102    #[test]
103    fn test_verification_buffer_different_chain_and_type() {
104        let chain = Chain::Arbitrum;
105        let sender = address!("0xABCD");
106        let message_type = MessageType::Aggregate;
107        let item_hash =
108            item_hash!("0000000000000000000000000000000000000000000000000000000000000001");
109
110        let buffer = verification_buffer(&chain, &sender, message_type, &item_hash);
111
112        assert_eq!(
113            buffer,
114            "ARB\n0xABCD\nAGGREGATE\n0000000000000000000000000000000000000000000000000000000000000001"
115        );
116    }
117
118    #[cfg(feature = "signature-evm")]
119    #[test]
120    fn test_verify_with_v_zero_format() {
121        // The fixture signature ends with 1b (v=27). Replacing the last byte
122        // with 00 (v=0) should be equivalent for recovery.
123        let json = include_str!("../../../../fixtures/messages/post/post.json");
124        let mut message: crate::message::Message = serde_json::from_str(json).unwrap();
125
126        // Original signature ends with "1b" (v=27); replace with "00" (v=0)
127        let sig = message.signature.as_str().to_string();
128        let normalized_sig = format!("{}00", &sig[..sig.len() - 2]);
129        message.signature = Signature::from(normalized_sig);
130
131        message.verify_signature().unwrap();
132    }
133}