espresso_types/
eth_signature_key.rs

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