thegraph_core/
signed_message.rs

1//! EIP-712 message signing and verification.
2//!
3//! This module provides the [`SignedMessage`] struct for signing and verifying messages according
4//! to the [EIP-712] standard.
5//!
6//! Available functions for interacting with messages:
7//!
8//! - [`sign`]: Signs a message using the EIP-712 standard.
9//! - [`recover_signer_address`]: Recovers the signer's address from a signed message.
10//! - [`verify`]: Convenience wrapper over [`recover_signer_address`] to verify the signer's
11//!   address.
12//!
13//! To use a Rust struct as a message, it must implement the [`ToSolStruct`] trait.
14//! Refer to the example below for more details.
15//!
16//! ## Example
17//! ```rust
18//! # use thegraph_core::alloy::{
19//! #    primitives::{Address, B256, address, b256, keccak256},
20//! #    sol_types::{eip712_domain, Eip712Domain},
21//! # };
22//! use thegraph_core::signed_message::{sign, verify, ToSolStruct};
23//!
24//! // Create a signer instance
25//! let signer = thegraph_core::alloy::signers::local::PrivateKeySigner::random();
26//!
27//! // Define the EIP-712 domain separator
28//! const DOMAIN: Eip712Domain = eip712_domain! {
29//!      name: "Example domain",
30//!      version: "1",
31//!      chain_id: 1,
32//!      verifying_contract: address!("a83682bbe91c0d2d48a13fd751b2da8e989fe421"),
33//!      salt: b256!("66eb090e6dbb9668c7d32c0ee7ba5e8f08d84385804485d316dd5f5692273593"),
34//! };
35//!
36//! // Define a message struct
37//! #[derive(Clone, Debug)]
38//! struct Message {
39//!    addr: Address,
40//!    hash: [u8; 32],
41//! }
42//!
43//! // Define the message equivalent solidity struct
44//! thegraph_core::alloy::sol! {
45//!     struct MessageSol {
46//!         address addr;
47//!         bytes32 hash;
48//!     }
49//! }
50//!
51//! // Implement the ToSolStruct trait for the message struct
52//! impl ToSolStruct<MessageSol> for Message {
53//!     fn to_sol_struct(&self) -> MessageSol {
54//!         MessageSol {
55//!            addr: self.addr,
56//!            hash: self.hash.into(),
57//!        }
58//!     }
59//! }
60//!
61//! // Create a message instance with some data
62//! let message = Message {
63//!    addr: address!("03f6d2a3d8c3413de72c193386f1894e1ddc2b6b"),
64//!    hash: *keccak256(b"Hello, world!"),
65//! };
66//!
67//! // Sign the message
68//! let signed_message = sign(&signer, &DOMAIN, message).expect("sign_message failed");
69//!
70//! // Verify the signed message
71//! assert!(verify(&DOMAIN, &signed_message, &signer.address()).is_ok());
72//! ```
73//!
74//! [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 "EIP-712"
75
76mod message;
77mod signing;
78
79pub use message::{MessageHash, SignatureBytes, SignedMessage, ToSolStruct};
80pub use signing::{
81    RecoverSignerError, SigningError, VerificationError, recover_signer_address, sign, verify,
82};
83
84#[cfg(test)]
85mod tests {
86    use alloy::{
87        primitives::{Signature, address, b256, keccak256},
88        signers::local::PrivateKeySigner,
89        sol_types::{Eip712Domain, eip712_domain},
90    };
91
92    use super::{message::SignedMessage, signing, signing::VerificationError};
93
94    /// Test EIP712 domain separator
95    const EIP712_DOMAIN: Eip712Domain = eip712_domain! {
96        name: "Test domain",
97        version: "1",
98        chain_id: 1,
99        verifying_contract: address!("a83682bbe91c0d2d48a13fd751b2da8e989fe421"),
100        salt: b256!("66eb090e6dbb9668c7d32c0ee7ba5e8f08d84385804485d316dd5f5692273593")
101    };
102
103    alloy::sol! {
104        /// Test struct for EIP712 message
105        struct Message {
106            bytes32 data;
107        }
108    }
109
110    /// Test utility method generating a random wallet
111    fn wallet() -> PrivateKeySigner {
112        PrivateKeySigner::random()
113    }
114
115    #[test]
116    fn sign_message_with_private_key_signer() {
117        //* Given
118        let signer = wallet();
119        let domain = EIP712_DOMAIN;
120
121        // Create a message with some data
122        let message = Message {
123            data: keccak256(b"Hello, world!"),
124        };
125
126        //* When
127        // Sign the message
128        let result = signing::sign(&signer, &domain, message);
129
130        //* Then
131        // The message should be signed
132        assert!(result.is_ok());
133    }
134
135    #[test]
136    fn recover_signer_from_signed_message() {
137        //* Given
138        let signer = wallet();
139
140        let domain = EIP712_DOMAIN;
141
142        // Create a message with some data
143        let message = Message {
144            data: keccak256(b"Hello, world!"),
145        };
146
147        // Sign the message
148        let signed_message = signing::sign(&signer, &domain, message).unwrap();
149
150        //* When
151        // Recover the signer's address
152        let result = signing::recover_signer_address(&domain, &signed_message);
153
154        //* Then
155        // The address should be recovered
156        let signer_address = result.expect("recover_signer failed");
157
158        // The signer should be the wallet's address
159        assert_eq!(signer_address, signer_address);
160    }
161
162    #[test]
163    fn recover_signer_should_fail_with_invalid_signature() {
164        //* Given
165        let domain = EIP712_DOMAIN;
166
167        // Create a message with some data
168        let message = Message {
169            data: keccak256(b"Hello, world!"),
170        };
171
172        // Create a signed message with an invalid signature (random values)
173        let invalid_signature_signed_message = SignedMessage {
174            message,
175            signature: Signature::from_scalars_and_parity(
176                b256!("ca457b3f821e5c03545944e0318868a783d0e6b438c85a82537d52a619decfe2"),
177                b256!("26a9f36fcf89431476aa556021ee77959dc480fb3458054f26d068b52d525cc4"),
178                false,
179            ),
180        };
181
182        //* When
183        // Recover the signer's address
184        let result = signing::recover_signer_address(&domain, &invalid_signature_signed_message);
185
186        //* Then
187        // The address should not be recovered
188        assert!(result.is_err());
189    }
190
191    #[test]
192    fn verify_signed_message() {
193        //* Given
194        let signer = wallet();
195        let signer_address = signer.address();
196
197        let domain = EIP712_DOMAIN;
198
199        let message = Message {
200            data: keccak256(b"Hello, world!"),
201        };
202
203        // Sign the message
204        let signed_message = signing::sign(&signer, &domain, message).unwrap();
205
206        //* When
207        // Verify the signed message
208        let result = signing::verify(&domain, &signed_message, &signer_address);
209
210        //* Then
211        // The signature should be valid
212        assert!(result.is_ok());
213    }
214
215    #[test]
216    fn signed_message_verification_should_fail_with_invalid_signer() {
217        //* Given
218        let signer = wallet();
219        let domain = EIP712_DOMAIN;
220
221        // Create a message with some data
222        let message = Message {
223            data: keccak256(b"Hello, world!"),
224        };
225
226        // Sign the message
227        let signed_message = signing::sign(&signer, &domain, message).unwrap();
228
229        // Create a different signer
230        let different_signer = wallet();
231        let different_signer_address = different_signer.address();
232
233        //* When
234        // Verify the signed message
235        let result = signing::verify(&domain, &signed_message, &different_signer_address);
236
237        //* Then
238        // The signature should be invalid
239        let error = result.expect_err("verify_signature should fail");
240        if let VerificationError::InvalidSigner { expected, received } = error {
241            assert_eq!(expected, different_signer_address);
242            assert_eq!(received, signer.address());
243        } else {
244            panic!("unexpected error: {:?}", error);
245        }
246    }
247}