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}