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, SignerError, SignerRepoModel, SignerType, TransactionRepoModel,
38    },
39    services::Signer,
40};
41
42use super::DataSignerTrait;
43
44use alloy::rpc::types::TransactionRequest;
45
46pub struct LocalSigner {
47    local_signer_client: AlloyLocalSignerClient<SigningKey>,
48}
49
50impl LocalSigner {
51    pub fn new(signer_model: &SignerRepoModel) -> Result<Self, SignerError> {
52        let config = signer_model
53            .config
54            .get_local()
55            .ok_or_else(|| SignerError::Configuration("Local config not found".to_string()))?;
56
57        let local_signer_client = {
58            let key_bytes = config.raw_key.borrow();
59
60            AlloyLocalSignerClient::from_bytes(&FixedBytes::from_slice(&key_bytes)).map_err(
61                |e| SignerError::Configuration(format!("Failed to create signer: {}", e)),
62            )?
63        };
64
65        Ok(Self {
66            local_signer_client,
67        })
68    }
69}
70
71impl From<AlloyAddress> for Address {
72    fn from(addr: AlloyAddress) -> Self {
73        Address::Evm(addr.into_array())
74    }
75}
76
77#[async_trait]
78impl Signer for LocalSigner {
79    async fn address(&self) -> Result<Address, SignerError> {
80        let address: Address = self.local_signer_client.address().into();
81        Ok(address)
82    }
83
84    async fn sign_transaction(
85        &self,
86        transaction: NetworkTransactionData,
87    ) -> Result<SignTransactionResponse, SignerError> {
88        let evm_data = transaction.get_evm_transaction_data()?;
89        if evm_data.is_eip1559() {
90            // Handle EIP-1559 transaction
91            let mut unsigned_tx = TxEip1559::try_from(transaction)?;
92
93            let signature = self
94                .local_signer_client
95                .sign_transaction(&mut unsigned_tx)
96                .await
97                .map_err(|e| {
98                    SignerError::SigningError(format!("Failed to sign EIP-1559 transaction: {e}"))
99                })?;
100
101            let signed_tx = unsigned_tx.into_signed(signature);
102            let mut signature_bytes = signature.as_bytes();
103
104            // Adjust v value for EIP-1559 (27/28 -> 0/1)
105            if signature_bytes[64] == 27 {
106                signature_bytes[64] = 0;
107            } else if signature_bytes[64] == 28 {
108                signature_bytes[64] = 1;
109            }
110
111            let mut raw = Vec::with_capacity(signed_tx.eip2718_encoded_length());
112            signed_tx.eip2718_encode(&mut raw);
113
114            Ok(SignTransactionResponse::Evm(SignTransactionResponseEvm {
115                hash: signed_tx.hash().to_string(),
116                signature: EvmTransactionDataSignature::from(&signature_bytes),
117                raw,
118            }))
119        } else {
120            // Handle legacy transaction
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        let ste = signature.as_bytes();
158
159        Ok(SignDataResponse::Evm(SignDataResponseEvm {
160            r: hex::encode(&ste[0..32]),
161            s: hex::encode(&ste[32..64]),
162            v: ste[64],
163            sig: hex::encode(ste),
164        }))
165    }
166
167    async fn sign_typed_data(
168        &self,
169        _typed_data: SignTypedDataRequest,
170    ) -> Result<SignDataResponse, SignerError> {
171        todo!()
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use secrets::SecretVec;
178
179    use crate::models::{EvmTransactionData, LocalSignerConfig, SignerConfig, U256};
180
181    use super::*;
182    use std::str::FromStr;
183
184    fn create_test_signer_model() -> SignerRepoModel {
185        let seed = vec![1u8; 32];
186        let raw_key = SecretVec::new(32, |v| v.copy_from_slice(&seed));
187        SignerRepoModel {
188            id: "test".to_string(),
189            config: SignerConfig::Local(LocalSignerConfig { raw_key }),
190        }
191    }
192
193    fn create_test_transaction() -> NetworkTransactionData {
194        NetworkTransactionData::Evm(EvmTransactionData {
195            from: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
196            to: Some("0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string()),
197            gas_price: Some(20000000000),
198            gas_limit: 21000,
199            nonce: Some(0),
200            value: U256::from(1000000000000000000u64),
201            data: Some("0x".to_string()),
202            chain_id: 1,
203            hash: None,
204            signature: None,
205            raw: None,
206            max_fee_per_gas: None,
207            max_priority_fee_per_gas: None,
208            speed: None,
209        })
210    }
211
212    #[tokio::test]
213    async fn test_address_generation() {
214        let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
215        let address = signer.address().await.unwrap();
216
217        match address {
218            Address::Evm(addr) => {
219                assert_eq!(addr.len(), 20); // EVM addresses are 20 bytes
220            }
221            _ => panic!("Expected EVM address"),
222        }
223    }
224
225    #[tokio::test]
226    async fn test_sign_transaction_invalid_data() {
227        let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
228        let mut tx = create_test_transaction();
229
230        if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
231            evm_tx.data = Some("invalid_hex".to_string());
232        }
233
234        let result = signer.sign_transaction(tx).await;
235        assert!(result.is_err());
236    }
237
238    #[tokio::test]
239    async fn test_sign_data() {
240        let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
241        let request = SignDataRequest {
242            message: "Test message".to_string(),
243        };
244
245        let result = signer.sign_data(request).await.unwrap();
246
247        match result {
248            SignDataResponse::Evm(sig) => {
249                assert_eq!(sig.r.len(), 64); // 32 bytes in hex
250                assert_eq!(sig.s.len(), 64); // 32 bytes in hex
251                assert!(sig.v == 27 || sig.v == 28); // Valid v values
252                assert_eq!(sig.sig.len(), 130); // 65 bytes in hex
253            }
254            _ => panic!("Expected EVM signature"),
255        }
256    }
257
258    #[tokio::test]
259    async fn test_sign_data_empty_message() {
260        let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
261        let request = SignDataRequest {
262            message: "".to_string(),
263        };
264
265        let result = signer.sign_data(request).await;
266        assert!(result.is_ok());
267    }
268
269    #[tokio::test]
270    async fn test_sign_transaction_with_contract_creation() {
271        let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
272        let mut tx = create_test_transaction();
273
274        if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
275            evm_tx.to = None;
276            evm_tx.data = Some("0x6080604000".to_string()); // Minimal valid hex string for test
277        }
278
279        let result = signer.sign_transaction(tx).await.unwrap();
280        match result {
281            SignTransactionResponse::Evm(signed_tx) => {
282                assert!(!signed_tx.hash.is_empty());
283                assert!(!signed_tx.raw.is_empty());
284                assert!(!signed_tx.signature.sig.is_empty());
285            }
286            _ => panic!("Expected EVM transaction response"),
287        }
288    }
289
290    #[tokio::test]
291    async fn test_sign_eip1559_transaction() {
292        let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
293        let mut tx = create_test_transaction();
294
295        // Convert to EIP-1559 transaction by setting max_fee_per_gas and max_priority_fee_per_gas
296        if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
297            evm_tx.gas_price = None;
298            evm_tx.max_fee_per_gas = Some(30_000_000_000);
299            evm_tx.max_priority_fee_per_gas = Some(2_000_000_000);
300        }
301
302        let result = signer.sign_transaction(tx).await;
303        assert!(result.is_ok());
304
305        match result.unwrap() {
306            SignTransactionResponse::Evm(signed_tx) => {
307                assert!(!signed_tx.hash.is_empty());
308                assert!(!signed_tx.raw.is_empty());
309                assert!(!signed_tx.signature.sig.is_empty());
310                // Verify signature components
311                assert_eq!(signed_tx.signature.r.len(), 64); // 32 bytes in hex
312                assert_eq!(signed_tx.signature.s.len(), 64); // 32 bytes in hex
313                assert!(signed_tx.signature.v == 0 || signed_tx.signature.v == 1);
314                // EIP-1559 v values
315            }
316            _ => panic!("Expected EVM transaction response"),
317        }
318    }
319
320    #[tokio::test]
321    async fn test_sign_eip1559_transaction_with_contract_creation() {
322        let signer = LocalSigner::new(&create_test_signer_model()).unwrap();
323        let mut tx = create_test_transaction();
324
325        if let NetworkTransactionData::Evm(ref mut evm_tx) = tx {
326            evm_tx.to = None;
327            evm_tx.data = Some("0x6080604000".to_string()); // Minimal valid hex string for test
328            evm_tx.gas_price = None;
329            evm_tx.max_fee_per_gas = Some(30_000_000_000);
330            evm_tx.max_priority_fee_per_gas = Some(2_000_000_000);
331        }
332
333        let result = signer.sign_transaction(tx).await;
334        assert!(result.is_ok());
335
336        match result.unwrap() {
337            SignTransactionResponse::Evm(signed_tx) => {
338                assert!(!signed_tx.hash.is_empty());
339                assert!(!signed_tx.raw.is_empty());
340                assert!(!signed_tx.signature.sig.is_empty());
341            }
342            _ => panic!("Expected EVM transaction response"),
343        }
344    }
345}