openzeppelin_relayer/services/signer/evm/
local_signer.rs1use alloy::{
17 consensus::{SignableTransaction, TxEip1559, TxLegacy},
18 network::{EthereumWallet, TransactionBuilder, TxSigner},
19 rpc::types::Transaction,
20 signers::{
21 k256::ecdsa::SigningKey, local::LocalSigner as AlloyLocalSignerClient,
22 Signer as AlloySigner, SignerSync,
23 },
24};
25
26use alloy::primitives::{address, Address as AlloyAddress, Bytes, FixedBytes, TxKind, U256};
27
28use async_trait::async_trait;
29
30use crate::{
31 domain::{
32 SignDataRequest, SignDataResponse, SignDataResponseEvm, SignTransactionResponse,
33 SignTransactionResponseEvm, SignTypedDataRequest,
34 },
35 models::{
36 Address, EvmTransactionData, EvmTransactionDataSignature, EvmTransactionDataTrait,
37 NetworkTransactionData, Signer as SignerDomainModel, SignerError, SignerRepoModel,
38 SignerType, TransactionRepoModel,
39 },
40 services::signer::{evm::construct_eip712_message_hash, Signer},
41};
42
43use super::{validate_and_format_signature, DataSignerTrait};
44
45use alloy::rpc::types::TransactionRequest;
46
47#[derive(Clone)]
48pub struct LocalSigner {
49 local_signer_client: AlloyLocalSignerClient<SigningKey>,
50}
51
52impl LocalSigner {
53 pub fn new(signer_model: &SignerDomainModel) -> Result<Self, SignerError> {
54 let config = signer_model
55 .config
56 .get_local()
57 .ok_or_else(|| SignerError::Configuration("Local config not found".to_string()))?;
58
59 let local_signer_client = {
60 let key_bytes = config.raw_key.borrow();
61
62 AlloyLocalSignerClient::from_bytes(&FixedBytes::from_slice(&key_bytes)).map_err(
63 |e| SignerError::Configuration(format!("Failed to create local signer: {e}")),
64 )?
65 };
66
67 Ok(Self {
68 local_signer_client,
69 })
70 }
71}
72
73impl From<AlloyAddress> for Address {
74 fn from(addr: AlloyAddress) -> Self {
75 Address::Evm(addr.into_array())
76 }
77}
78
79#[async_trait]
80impl Signer for LocalSigner {
81 async fn address(&self) -> Result<Address, SignerError> {
82 let address: Address = self.local_signer_client.address().into();
83 Ok(address)
84 }
85
86 async fn sign_transaction(
87 &self,
88 transaction: NetworkTransactionData,
89 ) -> Result<SignTransactionResponse, SignerError> {
90 let evm_data = transaction.get_evm_transaction_data()?;
91 if evm_data.is_eip1559() {
92 let mut unsigned_tx = TxEip1559::try_from(transaction)?;
93
94 let signature = self
95 .local_signer_client
96 .sign_transaction(&mut unsigned_tx)
97 .await
98 .map_err(|e| {
99 SignerError::SigningError(format!("Failed to sign EIP-1559 transaction: {e}"))
100 })?;
101
102 let signed_tx = unsigned_tx.into_signed(signature);
103 let mut signature_bytes = signature.as_bytes();
104
105 if signature_bytes[64] == 27 {
107 signature_bytes[64] = 0;
108 } else if signature_bytes[64] == 28 {
109 signature_bytes[64] = 1;
110 }
111
112 let mut raw = Vec::with_capacity(signed_tx.eip2718_encoded_length());
113 signed_tx.eip2718_encode(&mut raw);
114
115 Ok(SignTransactionResponse::Evm(SignTransactionResponseEvm {
116 hash: signed_tx.hash().to_string(),
117 signature: EvmTransactionDataSignature::from(&signature_bytes),
118 raw,
119 }))
120 } else {
121 let mut unsigned_tx = TxLegacy::try_from(transaction.clone())?;
122
123 let signature = self
124 .local_signer_client
125 .sign_transaction(&mut unsigned_tx)
126 .await
127 .map_err(|e| {
128 SignerError::SigningError(format!("Failed to sign legacy transaction: {e}"))
129 })?;
130
131 let signed_tx = unsigned_tx.into_signed(signature);
132 let signature_bytes = signature.as_bytes();
133
134 let mut raw = Vec::with_capacity(signed_tx.rlp_encoded_length());
135 signed_tx.rlp_encode(&mut raw);
136
137 Ok(SignTransactionResponse::Evm(SignTransactionResponseEvm {
138 hash: signed_tx.hash().to_string(),
139 signature: EvmTransactionDataSignature::from(&signature_bytes),
140 raw,
141 }))
142 }
143 }
144}
145
146#[async_trait]
147impl DataSignerTrait for LocalSigner {
148 async fn sign_data(&self, request: SignDataRequest) -> Result<SignDataResponse, SignerError> {
149 let message = request.message.as_bytes();
150
151 let signature = self
152 .local_signer_client
153 .sign_message(message)
154 .await
155 .map_err(|e| SignerError::SigningError(format!("Failed to sign message: {e}")))?;
156
157 validate_and_format_signature(&signature.as_bytes(), "Local")
158 }
159
160 async fn sign_typed_data(
161 &self,
162 request: SignTypedDataRequest,
163 ) -> Result<SignDataResponse, SignerError> {
164 let message_hash = construct_eip712_message_hash(&request)?;
165 let signature = self
166 .local_signer_client
167 .sign_hash(&message_hash.into())
168 .await
169 .map_err(|e| {
170 SignerError::SigningError(format!("Failed to sign EIP-712 message: {e}"))
171 })?;
172
173 validate_and_format_signature(&signature.as_bytes(), "Local")
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use secrets::SecretVec;
180
181 use crate::models::{EvmTransactionData, LocalSignerConfig, SignerConfig, U256};
182
183 use super::*;
184 use std::str::FromStr;
185
186 fn create_test_signer_model() -> SignerDomainModel {
187 let seed = vec![1u8; 32];
188 let raw_key = SecretVec::new(32, |v| v.copy_from_slice(&seed));
189 SignerDomainModel {
190 id: "test".to_string(),
191 config: SignerConfig::Local(LocalSignerConfig { raw_key }),
192 }
193 }
194
195 fn create_test_transaction() -> NetworkTransactionData {
196 NetworkTransactionData::Evm(EvmTransactionData {
197 from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
198 to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string()),
199 gas_price: Some(20000000000),
200 gas_limit: Some(21000),
201 nonce: Some(0),
202 value: U256::from(1000000000000000000u64),
203 data: Some("0x".to_string()),
204 chain_id: 1,
205 hash: None,
206 signature: None,
207 raw: None,
208 max_fee_per_gas: None,
209 max_priority_fee_per_gas: None,
210 speed: None,
211 })
212 }
213
214 #[tokio::test]
215 async fn test_address_generation() {
216 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
217 let address = signer.address().await.unwrap();
218
219 match address {
220 Address::Evm(addr) => {
221 assert_eq!(addr.len(), 20); }
223 _ => panic!("Expected EVM address"),
224 }
225 }
226
227 #[tokio::test]
228 async fn test_sign_transaction_invalid_data() {
229 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
230 let mut tx = create_test_transaction();
231
232 if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
233 evm_tx.data = Some("invalid_hex".to_string());
234 }
235
236 let result = signer.sign_transaction(tx).await;
237 assert!(result.is_err());
238 }
239
240 #[tokio::test]
241 async fn test_sign_data() {
242 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
243 let request = SignDataRequest {
244 message: "Test message".to_string(),
245 };
246
247 let result = signer.sign_data(request).await.unwrap();
248
249 match result {
250 SignDataResponse::Evm(sig) => {
251 assert_eq!(sig.r.len(), 64); assert_eq!(sig.s.len(), 64); assert!(sig.v == 27 || sig.v == 28); assert_eq!(sig.sig.len(), 130); }
256 _ => panic!("Expected EVM signature"),
257 }
258 }
259
260 #[tokio::test]
261 async fn test_sign_data_empty_message() {
262 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
263 let request = SignDataRequest {
264 message: "".to_string(),
265 };
266
267 let result = signer.sign_data(request).await;
268 assert!(result.is_ok());
269 }
270
271 #[tokio::test]
272 async fn test_sign_transaction_with_contract_creation() {
273 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
274 let mut tx = create_test_transaction();
275
276 if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
277 evm_tx.to = None;
278 evm_tx.data = Some("0x6080604000".to_string()); }
280
281 let result = signer.sign_transaction(tx).await.unwrap();
282 match result {
283 SignTransactionResponse::Evm(signed_tx) => {
284 assert!(!signed_tx.hash.is_empty());
285 assert!(!signed_tx.raw.is_empty());
286 assert!(!signed_tx.signature.sig.is_empty());
287 }
288 _ => panic!("Expected EVM transaction response"),
289 }
290 }
291
292 #[tokio::test]
293 async fn test_sign_eip1559_transaction() {
294 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
295 let mut tx = create_test_transaction();
296
297 if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
299 evm_tx.gas_price = None;
300 evm_tx.max_fee_per_gas = Some(30_000_000_000);
301 evm_tx.max_priority_fee_per_gas = Some(2_000_000_000);
302 }
303
304 let result = signer.sign_transaction(tx).await;
305 assert!(result.is_ok());
306
307 match result.unwrap() {
308 SignTransactionResponse::Evm(signed_tx) => {
309 assert!(!signed_tx.hash.is_empty());
310 assert!(!signed_tx.raw.is_empty());
311 assert!(!signed_tx.signature.sig.is_empty());
312 assert_eq!(signed_tx.signature.r.len(), 64); assert_eq!(signed_tx.signature.s.len(), 64); assert!(signed_tx.signature.v == 0 || signed_tx.signature.v == 1);
316 }
318 _ => panic!("Expected EVM transaction response"),
319 }
320 }
321
322 #[tokio::test]
323 async fn test_sign_eip1559_transaction_with_contract_creation() {
324 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
325 let mut tx = create_test_transaction();
326
327 if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
328 evm_tx.to = None;
329 evm_tx.data = Some("0x6080604000".to_string()); evm_tx.gas_price = None;
331 evm_tx.max_fee_per_gas = Some(30_000_000_000);
332 evm_tx.max_priority_fee_per_gas = Some(2_000_000_000);
333 }
334
335 let result = signer.sign_transaction(tx).await;
336 assert!(result.is_ok());
337
338 match result.unwrap() {
339 SignTransactionResponse::Evm(signed_tx) => {
340 assert!(!signed_tx.hash.is_empty());
341 assert!(!signed_tx.raw.is_empty());
342 assert!(!signed_tx.signature.sig.is_empty());
343 }
344 _ => panic!("Expected EVM transaction response"),
345 }
346 }
347
348 #[tokio::test]
349 async fn test_sign_typed_data() {
350 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
351
352 let domain_separator = "a".repeat(64);
354 let hash_struct = "b".repeat(64);
355
356 let request = SignTypedDataRequest {
357 domain_separator,
358 hash_struct_message: hash_struct,
359 };
360
361 let result = signer.sign_typed_data(request).await;
362 assert!(result.is_ok());
363
364 match result.unwrap() {
365 SignDataResponse::Evm(sig) => {
366 assert_eq!(sig.r.len(), 64); assert_eq!(sig.s.len(), 64); assert!(sig.v == 27 || sig.v == 28); assert_eq!(sig.sig.len(), 130); }
371 _ => panic!("Expected EVM signature"),
372 }
373 }
374
375 #[tokio::test]
376 async fn test_sign_typed_data_with_0x_prefix() {
377 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
378
379 let domain_separator = format!("0x{}", "a".repeat(64));
381 let hash_struct = format!("0x{}", "b".repeat(64));
382
383 let request = SignTypedDataRequest {
384 domain_separator,
385 hash_struct_message: hash_struct,
386 };
387
388 let result = signer.sign_typed_data(request).await;
389 assert!(result.is_ok());
390 }
391
392 #[tokio::test]
393 async fn test_sign_typed_data_invalid_domain_length() {
394 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
395
396 let domain_separator = "a".repeat(30);
398 let hash_struct = "b".repeat(64);
399
400 let request = SignTypedDataRequest {
401 domain_separator,
402 hash_struct_message: hash_struct,
403 };
404
405 let result = signer.sign_typed_data(request).await;
406 assert!(result.is_err());
407 match result {
408 Err(SignerError::SigningError(msg)) => {
409 assert!(msg.contains("Invalid domain separator length"));
410 }
411 _ => panic!("Expected SigningError for invalid domain length"),
412 }
413 }
414
415 #[tokio::test]
416 async fn test_sign_typed_data_invalid_hash_struct_length() {
417 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
418
419 let domain_separator = "a".repeat(64);
421 let hash_struct = "b".repeat(30);
422
423 let request = SignTypedDataRequest {
424 domain_separator,
425 hash_struct_message: hash_struct,
426 };
427
428 let result = signer.sign_typed_data(request).await;
429 assert!(result.is_err());
430 match result {
431 Err(SignerError::SigningError(msg)) => {
432 assert!(msg.contains("Invalid hash struct length"));
433 }
434 _ => panic!("Expected SigningError for invalid hash struct length"),
435 }
436 }
437
438 #[tokio::test]
439 async fn test_sign_typed_data_invalid_hex() {
440 let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
441
442 let domain_separator = "zzzzzzzz".to_string();
444 let hash_struct = "b".repeat(64);
445
446 let request = SignTypedDataRequest {
447 domain_separator,
448 hash_struct_message: hash_struct,
449 };
450
451 let result = signer.sign_typed_data(request).await;
452 assert!(result.is_err());
453 match result {
454 Err(SignerError::SigningError(msg)) => {
455 assert!(msg.contains("Invalid domain separator hex"));
456 }
457 _ => panic!("Expected SigningError for invalid hex"),
458 }
459 }
460}