openzeppelin_relayer/services/signer/evm/
local_signer.rs

1//! # EVM Local Signer Implementation
2//!
3//! This module provides a local signer implementation for Ethereum Virtual Machine (EVM)
4//! transactions and messages using the Alloy library with an in-memory private key.
5//!
6//! ## Features
7//!
8//! - Support for both legacy and EIP-1559 transaction types
9//! - Message signing with standard Ethereum prefixing
10//! - Implementation of the `DataSignerTrait` for EVM-specific operations
11//!
12//! ## Security Considerations
13//!
14//! This implementation stores private keys in memory and should primarily be used
15//! for development and testing purposes, not production
16use 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            // Adjust v value for EIP-1559 (27/28 -> 0/1)
106            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); // EVM addresses are 20 bytes
222            }
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); // 32 bytes in hex
252                assert_eq!(sig.s.len(), 64); // 32 bytes in hex
253                assert!(sig.v == 27 || sig.v == 28); // Valid v values
254                assert_eq!(sig.sig.len(), 130); // 65 bytes in hex
255            }
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()); // Minimal valid hex string for test
279        }
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        // Convert to EIP-1559 transaction by setting max_fee_per_gas and max_priority_fee_per_gas
298        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                // Verify signature components
313                assert_eq!(signed_tx.signature.r.len(), 64); // 32 bytes in hex
314                assert_eq!(signed_tx.signature.s.len(), 64); // 32 bytes in hex
315                assert!(signed_tx.signature.v == 0 || signed_tx.signature.v == 1);
316                // EIP-1559 v values
317            }
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()); // Minimal valid hex string for test
330            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        // Valid 32-byte hashes (64 hex characters each)
353        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); // 32 bytes in hex
367                assert_eq!(sig.s.len(), 64); // 32 bytes in hex
368                assert!(sig.v == 27 || sig.v == 28); // Valid v values
369                assert_eq!(sig.sig.len(), 130); // 65 bytes in hex
370            }
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        // Valid 32-byte hashes with 0x prefix
380        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        // Invalid domain separator (too short)
397        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        // Invalid hash struct (too short)
420        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        // Invalid hex characters
443        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}