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,
41};
42
43use super::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            // Handle EIP-1559 transaction
93            let mut unsigned_tx = TxEip1559::try_from(transaction)?;
94
95            let signature = self
96                .local_signer_client
97                .sign_transaction(&mut unsigned_tx)
98                .await
99                .map_err(|e| {
100                    SignerError::SigningError(format!("Failed to sign EIP-1559 transaction: {e}"))
101                })?;
102
103            let signed_tx = unsigned_tx.into_signed(signature);
104            let mut signature_bytes = signature.as_bytes();
105
106            // Adjust v value for EIP-1559 (27/28 -> 0/1)
107            if signature_bytes[64] == 27 {
108                signature_bytes[64] = 0;
109            } else if signature_bytes[64] == 28 {
110                signature_bytes[64] = 1;
111            }
112
113            let mut raw = Vec::with_capacity(signed_tx.eip2718_encoded_length());
114            signed_tx.eip2718_encode(&mut raw);
115
116            Ok(SignTransactionResponse::Evm(SignTransactionResponseEvm {
117                hash: signed_tx.hash().to_string(),
118                signature: EvmTransactionDataSignature::from(&signature_bytes),
119                raw,
120            }))
121        } else {
122            // Handle legacy transaction
123            let mut unsigned_tx = TxLegacy::try_from(transaction.clone())?;
124
125            let signature = self
126                .local_signer_client
127                .sign_transaction(&mut unsigned_tx)
128                .await
129                .map_err(|e| {
130                    SignerError::SigningError(format!("Failed to sign legacy transaction: {e}"))
131                })?;
132
133            let signed_tx = unsigned_tx.into_signed(signature);
134            let signature_bytes = signature.as_bytes();
135
136            let mut raw = Vec::with_capacity(signed_tx.rlp_encoded_length());
137            signed_tx.rlp_encode(&mut raw);
138
139            Ok(SignTransactionResponse::Evm(SignTransactionResponseEvm {
140                hash: signed_tx.hash().to_string(),
141                signature: EvmTransactionDataSignature::from(&signature_bytes),
142                raw,
143            }))
144        }
145    }
146}
147
148#[async_trait]
149impl DataSignerTrait for LocalSigner {
150    async fn sign_data(&self, request: SignDataRequest) -> Result<SignDataResponse, SignerError> {
151        let message = request.message.as_bytes();
152
153        let signature = self
154            .local_signer_client
155            .sign_message(message)
156            .await
157            .map_err(|e| SignerError::SigningError(format!("Failed to sign message: {}", e)))?;
158
159        let ste = signature.as_bytes();
160
161        Ok(SignDataResponse::Evm(SignDataResponseEvm {
162            r: hex::encode(&ste[0..32]),
163            s: hex::encode(&ste[32..64]),
164            v: ste[64],
165            sig: hex::encode(ste),
166        }))
167    }
168
169    async fn sign_typed_data(
170        &self,
171        _typed_data: SignTypedDataRequest,
172    ) -> Result<SignDataResponse, SignerError> {
173        todo!()
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}