espresso_types/
eth_signature_key.rs

1use std::{
2    fmt::{Display, Formatter},
3    hash::Hash,
4};
5
6use alloy::{
7    primitives::{Address, PrimitiveSignature},
8    signers::{
9        self,
10        k256::ecdsa::{SigningKey, VerifyingKey},
11        local::{
12            coins_bip39::{English, Mnemonic},
13            PrivateKeySigner,
14        },
15        utils::public_key_to_address,
16        SignerSync,
17    },
18};
19use alloy_compat::ethers_serde;
20use derive_more::*;
21use hotshot_types::traits::signature_key::{BuilderSignatureKey, PrivateSignatureKey};
22use serde::{Deserialize, Serialize};
23use thiserror::Error;
24
25use crate::FeeAccount;
26
27// Newtype because type doesn't implement Hash, Display, SerDe, Ord, PartialOrd
28#[derive(PartialEq, Eq, Clone)]
29pub struct EthKeyPair {
30    signing_key: SigningKey,
31    fee_account: FeeAccount,
32}
33
34impl TryFrom<&tagged_base64::TaggedBase64> for EthKeyPair {
35    type Error = tagged_base64::Tb64Error;
36
37    fn try_from(value: &tagged_base64::TaggedBase64) -> Result<Self, Self::Error> {
38        // Make sure the tag is correct
39        if value.tag() != "ETH_KEY_PAIR" {
40            return Err(tagged_base64::Tb64Error::InvalidTag);
41        }
42
43        // Convert the bytes to a signing key
44        let bytes = value.value();
45        let signing_key =
46            SigningKey::from_slice(&bytes).map_err(|_| tagged_base64::Tb64Error::InvalidData)?;
47
48        // Convert the signing key to an EthKeyPair
49        Ok(signing_key.into())
50    }
51}
52
53impl From<SigningKey> for EthKeyPair {
54    fn from(signing_key: SigningKey) -> Self {
55        let fee_account = public_key_to_address(&VerifyingKey::from(&signing_key)).into();
56        EthKeyPair {
57            signing_key,
58            fee_account,
59        }
60    }
61}
62
63impl PrivateSignatureKey for EthKeyPair {
64    fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
65        let signing_key =
66            SigningKey::from_slice(bytes).map_err(|_| tagged_base64::Tb64Error::InvalidData)?;
67
68        Ok(signing_key.into())
69    }
70
71    fn to_bytes(&self) -> Vec<u8> {
72        self.signing_key.to_bytes().to_vec()
73    }
74
75    fn to_tagged_base64(&self) -> Result<tagged_base64::TaggedBase64, tagged_base64::Tb64Error> {
76        tagged_base64::TaggedBase64::new("ETH_KEY_PAIR", &self.signing_key.to_bytes())
77    }
78}
79
80impl EthKeyPair {
81    pub fn from_mnemonic(
82        phrase: impl AsRef<str>,
83        index: impl Into<u32>,
84    ) -> Result<Self, signers::local::LocalSignerError> {
85        let index: u32 = index.into();
86        let mnemonic = Mnemonic::<English>::new_from_phrase(phrase.as_ref())?;
87        let derivation_path = format!("m/44'/60'/0'/0/{index}");
88        let derived_priv_key =
89            mnemonic.derive_key(derivation_path.as_str(), /* password */ None)?;
90        let signing_key: &SigningKey = derived_priv_key.as_ref();
91        Ok(signing_key.clone().into())
92    }
93
94    pub fn random() -> EthKeyPair {
95        SigningKey::random(&mut rand::thread_rng()).into()
96    }
97
98    pub fn fee_account(&self) -> FeeAccount {
99        self.fee_account
100    }
101
102    pub fn address(&self) -> Address {
103        self.fee_account.address()
104    }
105
106    pub fn signer(&self) -> PrivateKeySigner {
107        PrivateKeySigner::from(self.signing_key.clone())
108    }
109}
110
111impl Hash for EthKeyPair {
112    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
113        self.signing_key.to_bytes().hash(state);
114    }
115}
116
117// Always display the address, not the private key
118impl Display for EthKeyPair {
119    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
120        write!(f, "EthKeyPair(address={:?})", self.address())
121    }
122}
123
124impl std::fmt::Debug for EthKeyPair {
125    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
126        write!(f, "{}", self)
127    }
128}
129
130impl Serialize for EthKeyPair {
131    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
132        self.signing_key.to_bytes().serialize(serializer)
133    }
134}
135
136impl<'de> Deserialize<'de> for EthKeyPair {
137    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
138        let bytes = <[u8; 32]>::deserialize(deserializer)?;
139        Ok(EthKeyPair::from(
140            SigningKey::from_slice(&bytes).map_err(serde::de::Error::custom)?,
141        ))
142    }
143}
144
145impl PartialOrd for EthKeyPair {
146    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
147        Some(self.cmp(other))
148    }
149}
150
151impl Ord for EthKeyPair {
152    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
153        self.signing_key
154            .as_nonzero_scalar()
155            .cmp(other.signing_key.as_nonzero_scalar())
156    }
157}
158
159#[derive(self::Debug, Error)]
160#[error("Failed to sign builder message")]
161pub struct SigningError(#[from] signers::Error);
162
163/// signature type for the builder
164#[derive(
165    self::Debug, Clone, Copy, Hash, Deref, PartialEq, Eq, From, Into, Serialize, Deserialize,
166)]
167#[serde(transparent)]
168pub struct BuilderSignature(#[serde(with = "ethers_serde::signature")] pub PrimitiveSignature);
169
170impl BuilderSignatureKey for FeeAccount {
171    type BuilderPrivateKey = EthKeyPair;
172    type BuilderSignature = BuilderSignature;
173    type SignError = SigningError;
174
175    fn validate_builder_signature(&self, signature: &Self::BuilderSignature, data: &[u8]) -> bool {
176        signature.recover_address_from_msg(data).unwrap() == self.address()
177    }
178
179    fn sign_builder_message(
180        private_key: &Self::BuilderPrivateKey,
181        data: &[u8],
182    ) -> Result<Self::BuilderSignature, Self::SignError> {
183        let wallet = private_key.signer();
184        let message_hash = alloy::primitives::eip191_hash_message(data);
185        let sig = wallet
186            .sign_hash_sync(&message_hash)
187            .map_err(SigningError::from)?
188            .into();
189        Ok(sig)
190    }
191
192    fn generated_from_seed_indexed(seed: [u8; 32], index: u64) -> (Self, Self::BuilderPrivateKey) {
193        let mut hasher = blake3::Hasher::new();
194        hasher.update(&seed);
195        hasher.update(&index.to_le_bytes());
196        let new_seed = *hasher.finalize().as_bytes();
197        let signing_key = EthKeyPair::from(SigningKey::from_slice(&new_seed).unwrap());
198        (signing_key.fee_account(), signing_key)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use hotshot_types::traits::signature_key::BuilderSignatureKey;
205
206    use super::*;
207
208    impl EthKeyPair {
209        pub fn for_test() -> Self {
210            FeeAccount::generated_from_seed_indexed([0u8; 32], 0).1
211        }
212    }
213
214    #[test]
215    fn test_fmt() {
216        let key = EthKeyPair::for_test();
217        let expected = "EthKeyPair(address=0xb0cfa4e5893107e2995974ef032957752bb526e9)";
218        assert_eq!(format!("{}", key), expected);
219        assert_eq!(format!("{:?}", key), expected);
220    }
221
222    #[test]
223    fn test_derivation_from_mnemonic() {
224        let mnemonic = "test test test test test test test test test test test junk";
225        let key0 = EthKeyPair::from_mnemonic(mnemonic, 0u32).unwrap();
226        assert_eq!(
227            key0.address(),
228            Address::parse_checksummed("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", None).unwrap()
229        );
230        let key1 = EthKeyPair::from_mnemonic(mnemonic, 1u32).unwrap();
231        assert_eq!(
232            key1.address(),
233            Address::parse_checksummed("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", None).unwrap()
234        );
235    }
236
237    #[test]
238    fn test_key_serde() {
239        let key = EthKeyPair::for_test();
240        let serialized = bincode::serialize(&key).unwrap();
241        let deserialized: EthKeyPair = bincode::deserialize(&serialized).unwrap();
242        assert_eq!(key, deserialized);
243    }
244
245    #[test]
246    fn test_signing_and_recovery() {
247        // Recovery works
248        let key = EthKeyPair::for_test();
249        let msg = b"hello world";
250        let sig = FeeAccount::sign_builder_message(&key, msg).unwrap();
251        assert!(key.fee_account().validate_builder_signature(&sig, msg));
252
253        // Validation fails if signed with other key.
254        let other_key = FeeAccount::generated_from_seed_indexed([0u8; 32], 1).1;
255        let sig = FeeAccount::sign_builder_message(&other_key, msg).unwrap();
256        assert!(!key.fee_account().validate_builder_signature(&sig, msg));
257
258        // Validation fails if another message was signed
259        let sig = FeeAccount::sign_builder_message(&key, b"hello world XYZ").unwrap();
260        assert!(!key.fee_account().validate_builder_signature(&sig, msg));
261    }
262}