ether_link/
lib.rs

1use k256::{
2    ecdsa::SigningKey,
3    elliptic_curve::rand_core::OsRng,
4};
5use sha3::{Digest, Keccak256};
6
7pub struct EthKeyPair {
8    pub address: String,
9    pub privkey: String,
10    pub pubkey_compressed: String,
11    pub pubkey_uncompressed: String,
12}
13
14impl EthKeyPair {
15    // Generate a new Ethereum key pair
16    pub fn new() -> Self {
17        let private_key = SigningKey::random(&mut OsRng);
18        let privkey_hex = hex::encode(private_key.to_bytes());
19
20        let public_key = private_key.verifying_key();
21        // Compressed public key (starts with 0x02 or 0x03)
22        let pubkey_compressed = public_key.to_encoded_point(true).as_bytes().to_vec();
23        let pubkey_compressed_hex = hex::encode(&pubkey_compressed);
24        // Uncompressed public key (starts with 0x04)
25        let pubkey_uncompressed = public_key.to_encoded_point(false).as_bytes().to_vec();
26        let pubkey_uncompressed_hex = hex::encode(&pubkey_uncompressed);
27        
28        // Ethereum address (last 20 bytes of keccak256 of uncompressed key without prefix)
29        let hash = Keccak256::digest(&pubkey_uncompressed[1..]); // remove 0x04 prefix
30        let address = format!("0x{}", hex::encode(&hash[12..]));
31
32        Self {
33            address,
34            privkey: privkey_hex,
35            pubkey_compressed:pubkey_compressed_hex,
36            pubkey_uncompressed:pubkey_uncompressed_hex,
37        }
38    }
39    // Sign a message with the private key
40    pub async fn sign_message(&self, message: &str) -> Result<String, String> {
41        use ethers::core::types::Signature;
42        use ethers::signers::{LocalWallet, Signer};
43        use ethers::core::k256::SecretKey;
44        use std::time::{SystemTime, UNIX_EPOCH};
45        
46        // Convert hex private key to SecretKey
47        let secret_bytes = hex::decode(&self.privkey).map_err(|e| e.to_string())?;
48        let secret_key = SecretKey::from_slice(&secret_bytes).map_err(|e| e.to_string())?;
49        let wallet = LocalWallet::from(secret_key);
50        
51        let address = &self.address;
52        let timestamps = SystemTime::now()
53            .duration_since(UNIX_EPOCH)
54            .unwrap()
55            .as_secs();
56        let message_with_timestamp = format!("{}:{}", message, timestamps);
57        
58        // Verify the wallet address matches our derived address
59        let wallet_addr = format!("0x{}", hex::encode(wallet.address().as_bytes()));
60        debug_assert_eq!(wallet_addr.to_lowercase(), address.to_lowercase());
61
62        // Sign the hash with the private key
63        let signature: Signature = wallet
64            .sign_message(message_with_timestamp.clone())
65            .await
66            .map_err(|e| e.to_string())?;
67
68        // Return a JSON string containing both the message and signature for verification
69        let result = serde_json::json!({
70            "address": address,
71            "signed_message": message_with_timestamp,
72            "signature": format!("0x{}", hex::encode(signature.to_vec())),
73        });
74        Ok(result.to_string())
75    }
76}
77
78    // Verify the signature of a message
79    pub fn verify_ethereum_signature(message: &str, signature: &str, address: &str) -> Result<bool, String> {
80        use ethers::core::types::{Address, Signature};
81        use ethers::utils::hash_message;
82        use std::str::FromStr;
83
84        // Parse the signature and address
85        let signature = Signature::from_str(signature).map_err(|e| e.to_string())?;
86        let expected_address = Address::from_str(address).map_err(|e| e.to_string())?;
87        
88        // Hash the message as per Ethereum signed message format
89        let message_hash = hash_message(message);
90        
91        // Recover the address that signed the message
92        let recovered_address = signature.recover(message_hash).map_err(|e| e.to_string())?;
93        
94        // Compare the recovered address with the expected address
95        Ok(recovered_address == expected_address)
96    }
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use hex;
102
103    #[tokio::test]
104    async fn test_sign_message() {
105        let keypair = EthKeyPair::new();
106        let message = "Hello, world!";
107        let signature = keypair.sign_message(message).await.unwrap_or_else(|e| panic!("Failed to sign message: {}", e));
108        
109        // Parse the signature JSON string
110        let sig_json: serde_json::Value = serde_json::from_str(&signature).unwrap_or_else(|e| panic!("Failed to parse signature JSON: {}", e));
111        println!("Signature JSON: {}", serde_json::to_string_pretty(&sig_json).unwrap_or_else(|e| panic!("Failed to format JSON: {}", e)));
112        
113        // Verify the signature contains all expected fields
114        assert!(sig_json.get("address").is_some());
115        assert!(sig_json.get("signed_message").is_some());
116        assert!(sig_json.get("signature").is_some());
117    }
118
119    #[test]
120    fn test_eth_keypair_generation() {
121        let keypair = EthKeyPair::new();
122
123        // Check that the address is 42 chars: "0x" + 40 hex digits
124        assert_eq!(keypair.address.len(), 42);
125        assert!(keypair.address.starts_with("0x"));
126
127        // Check compressed key format (hex string should be 66 chars - 2 for each byte of the 33 byte key)
128        assert_eq!(keypair.pubkey_compressed.len(), 66);
129        
130        // Convert hex to bytes for byte checking
131        let compressed_bytes = hex::decode(&keypair.pubkey_compressed).unwrap();
132        assert!(
133            compressed_bytes[0] == 0x02 || compressed_bytes[0] == 0x03,
134            "Compressed pubkey should start with 0x02 or 0x03"
135        );
136
137        // Check uncompressed key format (hex string should be 130 chars - 2 for each byte of the 65 byte key)
138        assert_eq!(keypair.pubkey_uncompressed.len(), 130);
139        
140        // Convert hex to bytes for byte checking
141        let uncompressed_bytes = hex::decode(&keypair.pubkey_uncompressed).unwrap();
142        assert_eq!(uncompressed_bytes[0], 0x04, 
143            "Uncompressed pubkey should start with 0x04");
144
145        // Debug output
146        println!("Private Key: {}", keypair.privkey);
147        println!("Compressed Public Key: {}", keypair.pubkey_compressed);
148        println!("Uncompressed Public Key: {}", keypair.pubkey_uncompressed);
149        println!("Ethereum Address: {}", keypair.address);
150    }
151
152    #[tokio::test]
153    async fn test_check_signature_is_valid() {
154        let keypair = EthKeyPair::new();
155        let message = "Hello, world!";
156        
157        // Sign the message
158        let signature = keypair.sign_message(message)
159            .await
160            .unwrap_or_else(|e| panic!("Failed to sign message: {}", e));
161        
162        // Parse the signature JSON
163        let sig_json: serde_json::Value = serde_json::from_str(&signature)
164            .unwrap_or_else(|e| panic!("Failed to parse signature JSON: {}", e));
165        
166        // Get the signature components with better error handling
167        let address = sig_json["address"].as_str()
168            .expect("Missing 'address' field in signature JSON");
169        let signed_message = sig_json["signed_message"].as_str()
170            .expect("Missing 'signed_message' field in signature JSON"); 
171        let signature_hex = sig_json["signature"].as_str()
172            .expect("Missing 'signature' field in signature JSON");
173        
174        // Verify the signature matches the message and address
175        let is_valid = verify_ethereum_signature(
176            signed_message,
177            signature_hex,
178            address
179        ).expect("Signature verification failed");
180        
181        assert!(is_valid, "Signature verification failed");
182    }
183}