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