Skip to main content

reth_rpc/eth/helpers/
signer.rs

1//! An abstraction over ethereum signers.
2
3use alloy_dyn_abi::TypedData;
4use alloy_eips::eip2718::Decodable2718;
5use alloy_primitives::{map::AddressMap, Address, Signature, B256};
6use alloy_signer::SignerSync;
7use alloy_signer_local::{coins_bip39::English, MnemonicBuilder, PrivateKeySigner};
8use reth_rpc_convert::SignableTxRequest;
9use reth_rpc_eth_api::helpers::{signer::Result, EthSigner};
10use reth_rpc_eth_types::SignError;
11use std::sync::Arc;
12use tokio::sync::Mutex;
13
14/// Holds developer keys
15#[derive(Debug, Clone)]
16pub struct DevSigner {
17    addresses: Vec<Address>,
18    accounts: AddressMap<PrivateKeySigner>,
19    /// Shared by all clones of this signer, see [`EthSigner::nonce_lock`].
20    nonce_lock: Arc<Mutex<()>>,
21}
22
23impl DevSigner {
24    /// Generates provided number of random dev signers
25    /// which satisfy [`EthSigner`] trait
26    pub fn random_signers<T: Decodable2718, TxReq: SignableTxRequest<T>>(
27        num: u32,
28    ) -> Vec<Box<dyn EthSigner<T, TxReq> + 'static>> {
29        let mut signers = Vec::with_capacity(num as usize);
30        for _ in 0..num {
31            let sk = PrivateKeySigner::random();
32
33            let address = sk.address();
34            let addresses = vec![address];
35
36            let accounts = AddressMap::from_iter([(address, sk)]);
37            signers.push(Box::new(Self { addresses, accounts, nonce_lock: Default::default() })
38                as Box<dyn EthSigner<T, TxReq>>);
39        }
40        signers
41    }
42
43    /// Generates dev signers deterministically from a fixed mnemonic.
44    /// Uses the Ethereum derivation path: `m/44'/60'/0'/0/{index}`
45    pub fn from_mnemonic<T: Decodable2718, TxReq: SignableTxRequest<T>>(
46        mnemonic: &str,
47        num: u32,
48    ) -> Vec<Box<dyn EthSigner<T, TxReq> + 'static>> {
49        let mut signers = Vec::with_capacity(num as usize);
50
51        for i in 0..num {
52            let sk = MnemonicBuilder::<English>::default()
53                .phrase(mnemonic)
54                .index(i)
55                .expect("invalid derivation path")
56                .build()
57                .expect("failed to build signer from mnemonic");
58
59            let address = sk.address();
60            let addresses = vec![address];
61            let accounts = AddressMap::from_iter([(address, sk)]);
62
63            signers.push(Box::new(Self { addresses, accounts, nonce_lock: Default::default() })
64                as Box<dyn EthSigner<T, TxReq>>);
65        }
66
67        signers
68    }
69
70    fn get_key(&self, account: Address) -> Result<&PrivateKeySigner> {
71        self.accounts.get(&account).ok_or(SignError::NoAccount)
72    }
73
74    fn sign_hash(&self, hash: B256, account: Address) -> Result<Signature> {
75        let signature = self.get_key(account)?.sign_hash_sync(&hash);
76        signature.map_err(|_| SignError::CouldNotSign)
77    }
78}
79
80#[async_trait::async_trait]
81impl<T: Decodable2718, TxReq: SignableTxRequest<T>> EthSigner<T, TxReq> for DevSigner {
82    fn accounts(&self) -> Vec<Address> {
83        self.addresses.clone()
84    }
85
86    fn is_signer_for(&self, addr: &Address) -> bool {
87        self.accounts.contains_key(addr)
88    }
89
90    async fn sign(&self, address: Address, message: &[u8]) -> Result<Signature> {
91        self.get_key(address)?.sign_message_sync(message).map_err(|_| SignError::CouldNotSign)
92    }
93
94    async fn sign_transaction(&self, request: TxReq, address: &Address) -> Result<T> {
95        // create local signer wallet from signing key
96        let signer = self.accounts.get(address).ok_or(SignError::NoAccount)?.clone();
97
98        // build and sign transaction with signer
99        let tx = request
100            .try_build_and_sign(&signer)
101            .await
102            .map_err(|_| SignError::InvalidTransactionRequest)?;
103
104        Ok(tx)
105    }
106
107    fn sign_typed_data(&self, address: Address, payload: &TypedData) -> Result<Signature> {
108        let encoded = payload.eip712_signing_hash().map_err(|_| SignError::InvalidTypedData)?;
109        self.sign_hash(encoded, address)
110    }
111
112    fn nonce_lock(&self) -> Option<Arc<Mutex<()>>> {
113        Some(self.nonce_lock.clone())
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use alloy_consensus::Transaction;
121    use alloy_primitives::{Bytes, TxKind, U256};
122    use alloy_rpc_types_eth::{TransactionInput, TransactionRequest};
123    use reth_ethereum_primitives::TransactionSigned;
124
125    fn build_signer() -> DevSigner {
126        let signer: PrivateKeySigner =
127            "4646464646464646464646464646464646464646464646464646464646464646".parse().unwrap();
128        let address = signer.address();
129        let accounts = AddressMap::from_iter([(address, signer)]);
130        let addresses = vec![address];
131        DevSigner { addresses, accounts, nonce_lock: Default::default() }
132    }
133
134    #[tokio::test]
135    async fn test_sign_type_data() {
136        let eip_712_example = r#"{
137            "types": {
138            "EIP712Domain": [
139                {
140                    "name": "name",
141                    "type": "string"
142                },
143                {
144                    "name": "version",
145                    "type": "string"
146                },
147                {
148                    "name": "chainId",
149                    "type": "uint256"
150                },
151                {
152                    "name": "verifyingContract",
153                    "type": "address"
154                }
155            ],
156            "Person": [
157                {
158                    "name": "name",
159                    "type": "string"
160                },
161                {
162                    "name": "wallet",
163                    "type": "address"
164                }
165            ],
166            "Mail": [
167                {
168                    "name": "from",
169                    "type": "Person"
170                },
171                {
172                    "name": "to",
173                    "type": "Person"
174                },
175                {
176                    "name": "contents",
177                    "type": "string"
178                }
179            ]
180        },
181        "primaryType": "Mail",
182        "domain": {
183            "name": "Ether Mail",
184            "version": "1",
185            "chainId": 1,
186            "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
187        },
188        "message": {
189            "from": {
190                "name": "Cow",
191                "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
192            },
193            "to": {
194                "name": "Bob",
195                "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
196            },
197            "contents": "Hello, Bob!"
198        }
199        }"#;
200        let data: TypedData = serde_json::from_str(eip_712_example).unwrap();
201        let signer = build_signer();
202        let from = *signer.addresses.first().unwrap();
203        let sig = EthSigner::<reth_ethereum_primitives::TransactionSigned>::sign_typed_data(
204            &signer, from, &data,
205        )
206        .unwrap();
207        let expected = Signature::new(
208            U256::from_str_radix(
209                "5318aee9942b84885761bb20e768372b76e7ee454fc4d39b59ce07338d15a06c",
210                16,
211            )
212            .unwrap(),
213            U256::from_str_radix(
214                "5e585a2f4882ec3228a9303244798b47a9102e4be72f48159d890c73e4511d79",
215                16,
216            )
217            .unwrap(),
218            false,
219        );
220        assert_eq!(sig, expected)
221    }
222
223    #[tokio::test]
224    async fn test_signer() {
225        let message = b"Test message";
226        let signer = build_signer();
227        let from = *signer.addresses.first().unwrap();
228        let sig =
229            EthSigner::<reth_ethereum_primitives::TransactionSigned>::sign(&signer, from, message)
230                .await
231                .unwrap();
232        let expected = Signature::new(
233            U256::from_str_radix(
234                "54313da7432e4058b8d22491b2e7dbb19c7186c35c24155bec0820a8a2bfe0c1",
235                16,
236            )
237            .unwrap(),
238            U256::from_str_radix(
239                "687250f11a3d4435004c04a4cb60e846bc27997271d67f21c6c8170f17a25e10",
240                16,
241            )
242            .unwrap(),
243            true,
244        );
245        assert_eq!(sig, expected)
246    }
247
248    #[tokio::test]
249    async fn test_sign_transaction() {
250        let message = b"Test message";
251        let signer = build_signer();
252        let from = *signer.addresses.first().unwrap();
253        let request = TransactionRequest {
254            chain_id: Some(1u64),
255            from: Some(from),
256            to: Some(TxKind::Create),
257            gas: Some(1000),
258            gas_price: Some(1000u128),
259            value: Some(U256::from(1000)),
260            input: TransactionInput {
261                data: Some(Bytes::from(message)),
262                input: Some(Bytes::from(message)),
263            },
264            nonce: Some(0u64),
265            ..Default::default()
266        };
267        let txn_signed: std::result::Result<TransactionSigned, SignError> =
268            signer.sign_transaction(request, &from).await;
269        assert!(txn_signed.is_ok());
270
271        assert_eq!(Bytes::from(message), txn_signed.unwrap().input().0);
272    }
273}