espresso_types/
eth_signature_key.rs1use 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#[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 if value.tag() != "ETH_KEY_PAIR" {
39 return Err(tagged_base64::Tb64Error::InvalidTag);
40 }
41
42 let bytes = value.value();
44 let signing_key =
45 SigningKey::from_slice(&bytes).map_err(|_| tagged_base64::Tb64Error::InvalidData)?;
46
47 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(), 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
116impl 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 PartialOrd for EthKeyPair {
130 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
131 Some(self.cmp(other))
132 }
133}
134
135impl Ord for EthKeyPair {
136 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
137 self.signing_key
138 .as_nonzero_scalar()
139 .cmp(other.signing_key.as_nonzero_scalar())
140 }
141}
142
143#[derive(self::Debug, Error)]
144#[error("Failed to sign builder message")]
145pub struct SigningError(#[from] signers::Error);
146
147#[derive(
149 self::Debug, Clone, Copy, Hash, Deref, PartialEq, Eq, From, Into, Serialize, Deserialize,
150)]
151#[serde(transparent)]
152pub struct BuilderSignature(#[serde(with = "ethers_serde::signature")] pub Signature);
153
154impl BuilderSignatureKey for FeeAccount {
155 type BuilderPrivateKey = EthKeyPair;
156 type BuilderSignature = BuilderSignature;
157 type SignError = SigningError;
158
159 fn validate_builder_signature(&self, signature: &Self::BuilderSignature, data: &[u8]) -> bool {
160 signature.recover_address_from_msg(data).unwrap() == self.address()
161 }
162
163 fn sign_builder_message(
164 private_key: &Self::BuilderPrivateKey,
165 data: &[u8],
166 ) -> Result<Self::BuilderSignature, Self::SignError> {
167 let wallet = private_key.signer();
168 let message_hash = alloy::primitives::eip191_hash_message(data);
169 let sig = wallet
170 .sign_hash_sync(&message_hash)
171 .map_err(SigningError::from)?
172 .into();
173 Ok(sig)
174 }
175
176 fn generated_from_seed_indexed(seed: [u8; 32], index: u64) -> (Self, Self::BuilderPrivateKey) {
177 let mut hasher = blake3::Hasher::new();
178 hasher.update(&seed);
179 hasher.update(&index.to_le_bytes());
180 let new_seed = *hasher.finalize().as_bytes();
181 let signing_key = EthKeyPair::from(SigningKey::from_slice(&new_seed).unwrap());
182 (signing_key.fee_account(), signing_key)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use hotshot_types::traits::signature_key::BuilderSignatureKey;
189
190 use super::*;
191
192 impl EthKeyPair {
193 pub fn for_test() -> Self {
194 FeeAccount::generated_from_seed_indexed([0u8; 32], 0).1
195 }
196 }
197
198 #[test]
199 fn test_fmt() {
200 let key = EthKeyPair::for_test();
201 let expected = "EthKeyPair(address=0xb0cfa4e5893107e2995974ef032957752bb526e9)";
202 assert_eq!(format!("{key}"), expected);
203 assert_eq!(format!("{key:?}"), expected);
204 }
205
206 #[test]
207 fn test_derivation_from_mnemonic() {
208 let mnemonic = "test test test test test test test test test test test junk";
209 let key0 = EthKeyPair::from_mnemonic(mnemonic, 0u32).unwrap();
210 assert_eq!(
211 key0.address(),
212 Address::parse_checksummed("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", None).unwrap()
213 );
214 let key1 = EthKeyPair::from_mnemonic(mnemonic, 1u32).unwrap();
215 assert_eq!(
216 key1.address(),
217 Address::parse_checksummed("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", None).unwrap()
218 );
219 }
220
221 #[test]
222 fn test_signing_and_recovery() {
223 let key = EthKeyPair::for_test();
225 let msg = b"hello world";
226 let sig = FeeAccount::sign_builder_message(&key, msg).unwrap();
227 assert!(key.fee_account().validate_builder_signature(&sig, msg));
228
229 let other_key = FeeAccount::generated_from_seed_indexed([0u8; 32], 1).1;
231 let sig = FeeAccount::sign_builder_message(&other_key, msg).unwrap();
232 assert!(!key.fee_account().validate_builder_signature(&sig, msg));
233
234 let sig = FeeAccount::sign_builder_message(&key, b"hello world XYZ").unwrap();
236 assert!(!key.fee_account().validate_builder_signature(&sig, msg));
237 }
238}