Skip to main content

chains_sdk/ethereum/
mod.rs

1//! Ethereum ECDSA signer using secp256k1 + Keccak-256.
2//!
3//! Implements EIP-2 Low-S normalization, recovery ID (v, r, s),
4//! Ethereum address derivation, EIP-155/191/712 signing, and
5//! typed transaction encoding (EIP-2718/2930/1559/4844).
6
7pub mod abi;
8pub mod eips;
9pub mod keystore;
10pub mod rlp;
11pub mod siwe;
12pub mod transaction;
13
14/// BLS12-381 for Ethereum Proof-of-Stake (Beacon Chain).
15///
16/// Re-exported from `crate::bls` since BLS12-381 is fundamentally
17/// an Ethereum consensus layer primitive.
18#[cfg(feature = "bls")]
19pub use crate::bls;
20
21use crate::error::SignerError;
22use crate::traits;
23use k256::ecdsa::{RecoveryId, Signature as K256Signature, SigningKey, VerifyingKey};
24use sha3::{Digest, Keccak256};
25use subtle::ConstantTimeEq;
26use zeroize::Zeroizing;
27
28/// An Ethereum ECDSA signature with recovery ID.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[must_use]
32pub struct EthereumSignature {
33    /// The R component (32 bytes).
34    pub r: [u8; 32],
35    /// The S component (32 bytes), guaranteed to be low-S (EIP-2).
36    pub s: [u8; 32],
37    /// Recovery ID: 27 or 28 (legacy), or chain_id * 2 + 35 + rec_id (EIP-155).
38    /// Stored as `u64` to support high chain IDs (Polygon=137, Arbitrum=42161, etc.).
39    pub v: u64,
40}
41
42impl core::fmt::Display for EthereumSignature {
43    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
44        write!(f, "0x")?;
45        for byte in &self.r {
46            write!(f, "{byte:02x}")?;
47        }
48        for byte in &self.s {
49            write!(f, "{byte:02x}")?;
50        }
51        write!(f, "{:x}", self.v)
52    }
53}
54
55impl EthereumSignature {
56    /// Encode as 65-byte `r || s || v` (legacy, v truncated to u8).
57    ///
58    /// For EIP-155 signatures with high chain IDs, use `to_bytes_eip155()` instead.
59    pub fn to_bytes(&self) -> [u8; 65] {
60        let mut out = [0u8; 65];
61        out[..32].copy_from_slice(&self.r);
62        out[32..64].copy_from_slice(&self.s);
63        out[64] = self.v as u8;
64        out
65    }
66
67    /// Encode as `r (32) || s (32) || v (8 bytes BE)` for EIP-155 with large chain IDs.
68    pub fn to_bytes_eip155(&self) -> Vec<u8> {
69        let mut out = Vec::with_capacity(72);
70        out.extend_from_slice(&self.r);
71        out.extend_from_slice(&self.s);
72        out.extend_from_slice(&self.v.to_be_bytes());
73        out
74    }
75
76    /// Decode from 65-byte `r || s || v` (v as single byte).
77    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
78        if bytes.len() != 65 {
79            return Err(SignerError::InvalidSignature(format!(
80                "expected 65 bytes, got {}",
81                bytes.len()
82            )));
83        }
84        let mut r = [0u8; 32];
85        let mut s = [0u8; 32];
86        r.copy_from_slice(&bytes[..32]);
87        s.copy_from_slice(&bytes[32..64]);
88        Ok(Self { r, s, v: u64::from(bytes[64]) })
89    }
90
91    /// Extract the recovery bit (0 or 1) from `v`, handling both
92    /// legacy (v=27/28) and EIP-155 (v = chain_id*2 + 35 + {0,1}).
93    pub fn recovery_bit(&self) -> u8 {
94        if self.v >= 35 {
95            // EIP-155: recovery_bit = (v - 35) % 2
96            ((self.v - 35) % 2) as u8
97        } else {
98            // Legacy: v = 27 or 28
99            (self.v.wrapping_sub(27) & 1) as u8
100        }
101    }
102}
103
104/// Ethereum ECDSA signer.
105///
106/// Wraps a secp256k1 `SigningKey` and applies Keccak-256 hashing,
107/// EIP-2 Low-S normalization, and recovery ID calculation.
108pub struct EthereumSigner {
109    signing_key: SigningKey,
110}
111
112impl Drop for EthereumSigner {
113    fn drop(&mut self) {
114        // k256::SigningKey implements ZeroizeOnDrop internally
115    }
116}
117
118impl EthereumSigner {
119    /// Derive the Ethereum address from this signer's public key.
120    /// Returns the 20-byte address (last 20 bytes of keccak256(uncompressed_pubkey[1..])).
121    pub fn address(&self) -> [u8; 20] {
122        let vk = self.signing_key.verifying_key();
123        let point = vk.to_encoded_point(false);
124        let pubkey_bytes = &point.as_bytes()[1..]; // skip the 0x04 prefix
125        let hash = Keccak256::digest(pubkey_bytes);
126        let mut addr = [0u8; 20];
127        addr.copy_from_slice(&hash[12..]);
128        addr
129    }
130
131    /// Sign a pre-hashed 32-byte digest and return the Ethereum signature.
132    fn sign_digest(&self, digest: &[u8; 32]) -> Result<EthereumSignature, SignerError> {
133        let (sig, rec_id) = self
134            .signing_key
135            .sign_prehash_recoverable(digest)
136            .map_err(|e| SignerError::SigningFailed(e.to_string()))?;
137
138        let mut r_bytes = [0u8; 32];
139        let mut s_bytes = [0u8; 32];
140        let sig_bytes = sig.to_bytes();
141        r_bytes.copy_from_slice(&sig_bytes[..32]);
142        s_bytes.copy_from_slice(&sig_bytes[32..]);
143
144        // EIP-2 Low-S normalization
145        let mut v = rec_id.to_byte();
146        let sig_normalized = sig.normalize_s();
147        if let Some(normalized) = sig_normalized {
148            let norm_bytes = normalized.to_bytes();
149            s_bytes.copy_from_slice(&norm_bytes[32..]);
150            // Flip recovery ID when S is normalized
151            v ^= 1;
152        }
153
154        Ok(EthereumSignature {
155            r: r_bytes,
156            s: s_bytes,
157            v: 27 + u64::from(v),
158        })
159    }
160
161    /// **EIP-712**: Sign typed structured data.
162    ///
163    /// Computes `keccak256("\x19\x01" || domain_separator || struct_hash)` and signs it.
164    ///
165    /// - `domain_separator`: 32-byte keccak256 hash of the EIP-712 domain (see [`Eip712Domain::separator`]).
166    /// - `struct_hash`: 32-byte keccak256 hash of the typed struct (computed by the caller).
167    pub fn sign_typed_data(
168        &self,
169        domain_separator: &[u8; 32],
170        struct_hash: &[u8; 32],
171    ) -> Result<EthereumSignature, SignerError> {
172        let digest = eip712_hash(domain_separator, struct_hash);
173        self.sign_digest(&digest)
174    }
175
176    /// **EIP-191**: Sign a personal message (as used by MetaMask `personal_sign`).
177    ///
178    /// Computes `keccak256("\x19Ethereum Signed Message:\n{len}{message}")` and signs it.
179    /// This is the standard for off-chain message signing in Ethereum wallets.
180    pub fn personal_sign(&self, message: &[u8]) -> Result<EthereumSignature, SignerError> {
181        let digest = eip191_hash(message);
182        self.sign_digest(&digest)
183    }
184
185    /// **EIP-155**: Sign a message with chain-specific replay protection.
186    ///
187    /// Produces `v = {0,1} + chain_id * 2 + 35` instead of `v = 27/28`.
188    /// This is required for mainnet Ethereum transactions since the Spurious Dragon fork.
189    ///
190    /// Common chain IDs: 1 (mainnet), 5 (Goerli), 11155111 (Sepolia), 137 (Polygon).
191    pub fn sign_with_chain_id(
192        &self,
193        message: &[u8],
194        chain_id: u64,
195    ) -> Result<EthereumSignature, SignerError> {
196        let digest = Keccak256::digest(message);
197        let mut hash = [0u8; 32];
198        hash.copy_from_slice(&digest);
199        self.sign_digest_with_chain_id(&hash, chain_id)
200    }
201
202    /// **EIP-155**: Sign a pre-hashed digest with chain-specific replay protection.
203    pub fn sign_digest_with_chain_id(
204        &self,
205        digest: &[u8; 32],
206        chain_id: u64,
207    ) -> Result<EthereumSignature, SignerError> {
208        let mut sig = self.sign_digest(digest)?;
209        // Convert v from legacy (27/28) to EIP-155 (35 + chain_id*2 + {0,1})
210        let recovery_bit = sig.v - 27; // 0 or 1
211        sig.v = recovery_bit
212            .checked_add(
213                chain_id
214                    .checked_mul(2)
215                    .ok_or_else(|| SignerError::SigningFailed("chain_id overflow".into()))?,
216            )
217            .and_then(|v| v.checked_add(35))
218            .ok_or_else(|| SignerError::SigningFailed("EIP-155 v overflow".into()))?;
219        Ok(sig)
220    }
221
222    /// **EIP-191**: Sign a personal message with chain-specific v value.
223    pub fn personal_sign_with_chain_id(
224        &self,
225        message: &[u8],
226        chain_id: u64,
227    ) -> Result<EthereumSignature, SignerError> {
228        let digest = eip191_hash(message);
229        self.sign_digest_with_chain_id(&digest, chain_id)
230    }
231
232    /// Return the EIP-55 checksummed hex address string (e.g., `0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B`).
233    pub fn address_checksum(&self) -> String {
234        eip55_checksum(&self.address())
235    }
236
237    /// Create an `EthereumSigner` from a BIP-39 mnemonic phrase.
238    ///
239    /// Uses the standard Ethereum HD derivation path `m/44'/60'/0'/0/{index}`.
240    ///
241    /// # Arguments
242    /// - `phrase` — BIP-39 mnemonic words (12 or 24 words)
243    /// - `passphrase` — Optional BIP-39 passphrase (empty string for none)
244    /// - `index` — Account index (0 for the first account)
245    ///
246    /// # Example
247    /// ```no_run
248    /// use chains_sdk::ethereum::EthereumSigner;
249    ///
250    /// let signer = EthereumSigner::from_mnemonic(
251    ///     "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
252    ///     "",
253    ///     0,
254    /// ).unwrap();
255    /// println!("Address: {}", signer.address_checksum());
256    /// ```
257    pub fn from_mnemonic(phrase: &str, passphrase: &str, index: u32) -> Result<Self, SignerError> {
258        use crate::hd_key::{DerivationPath, ExtendedPrivateKey};
259        use crate::mnemonic::Mnemonic;
260        use crate::traits::KeyPair;
261
262        let mnemonic = Mnemonic::from_phrase(phrase)?;
263        let seed = mnemonic.to_seed(passphrase);
264        let master = ExtendedPrivateKey::from_seed(&*seed)?;
265        let path = DerivationPath::ethereum(index);
266        let child = master.derive_path(&path)?;
267        let private_key = child.private_key_bytes();
268        Self::from_bytes(&private_key)
269    }
270}
271
272/// Return an EIP-55 checksummed hex address string from 20 raw bytes.
273///
274/// The EIP-55 spec: hex-encode the address, then uppercase each hex digit
275/// whose corresponding nibble in the keccak256 of the lowercase hex is >= 8.
276pub fn eip55_checksum(address: &[u8; 20]) -> String {
277    let hex_lower: String = address.iter().map(|b| format!("{b:02x}")).collect();
278    let hash = Keccak256::digest(hex_lower.as_bytes());
279    let mut out = String::with_capacity(42);
280    out.push_str("0x");
281    for (i, c) in hex_lower.chars().enumerate() {
282        let hash_nibble = if i % 2 == 0 {
283            (hash[i / 2] >> 4) & 0x0f
284        } else {
285            hash[i / 2] & 0x0f
286        };
287        if hash_nibble >= 8 {
288            out.extend(c.to_uppercase());
289        } else {
290            out.push(c);
291        }
292    }
293    out
294}
295
296/// Validate an Ethereum address string.
297///
298/// - Must start with `0x` and be 42 characters total
299/// - Must contain only valid hexadecimal characters
300/// - If mixed-case, verifies the EIP-55 checksum
301pub fn validate_address(address: &str) -> bool {
302    if address.len() != 42 || !address.starts_with("0x") {
303        return false;
304    }
305    let hex_part = &address[2..];
306    // Verify all characters are valid hex
307    if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
308        return false;
309    }
310    // If all lowercase or all uppercase, skip checksum verification
311    let has_upper = hex_part.chars().any(|c| c.is_ascii_uppercase());
312    let has_lower = hex_part.chars().any(|c| c.is_ascii_lowercase());
313    if !has_upper || !has_lower {
314        return true; // all-lower or all-upper is valid without checksum
315    }
316    // Mixed case → verify EIP-55 checksum
317    let lower = hex_part.to_lowercase();
318    let lower_bytes = lower.as_bytes();
319    let mut bytes = [0u8; 20];
320    for (i, chunk) in lower_bytes.chunks_exact(2).enumerate() {
321        let hi = match chunk[0] {
322            b'0'..=b'9' => chunk[0] - b'0',
323            b'a'..=b'f' => chunk[0] - b'a' + 10,
324            _ => return false,
325        };
326        let lo = match chunk[1] {
327            b'0'..=b'9' => chunk[1] - b'0',
328            b'a'..=b'f' => chunk[1] - b'a' + 10,
329            _ => return false,
330        };
331        bytes[i] = (hi << 4) | lo;
332    }
333    let checksummed = eip55_checksum(&bytes);
334    checksummed == address
335}
336
337/// **ecrecover**: Recover the signer's Ethereum address from a message and signature.
338///
339/// Internally keccak256-hashes the message and performs ECDSA recovery.
340/// Returns the 20-byte address of the signer.
341pub fn ecrecover(message: &[u8], signature: &EthereumSignature) -> Result<[u8; 20], SignerError> {
342    let digest = Keccak256::digest(message);
343    let mut hash = [0u8; 32];
344    hash.copy_from_slice(&digest);
345    ecrecover_digest(&hash, signature)
346}
347
348/// **ecrecover** from a pre-hashed 32-byte digest, useful for EIP-712 / EIP-191.
349pub fn ecrecover_digest(
350    digest: &[u8; 32],
351    signature: &EthereumSignature,
352) -> Result<[u8; 20], SignerError> {
353    let rec_id = RecoveryId::try_from(signature.recovery_bit()).map_err(|_| {
354        SignerError::InvalidSignature("invalid recovery id".into())
355    })?;
356
357    let mut sig_bytes = [0u8; 64];
358    sig_bytes[..32].copy_from_slice(&signature.r);
359    sig_bytes[32..].copy_from_slice(&signature.s);
360    let sig = K256Signature::from_bytes((&sig_bytes).into())
361        .map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
362
363    let recovered_key = VerifyingKey::recover_from_prehash(digest, &sig, rec_id)
364        .map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
365
366    let point = recovered_key.to_encoded_point(false);
367    let pubkey_bytes = &point.as_bytes()[1..];
368    let hash = Keccak256::digest(pubkey_bytes);
369    let mut addr = [0u8; 20];
370    addr.copy_from_slice(&hash[12..]);
371    Ok(addr)
372}
373
374/// Compute the EIP-191 personal message hash:
375/// `keccak256("\x19Ethereum Signed Message:\n" || len(message) || message)`
376///
377/// Uses stack-based formatting to avoid heap allocation.
378pub fn eip191_hash(message: &[u8]) -> [u8; 32] {
379    use core::fmt::Write;
380    // Max message length decimal digits: usize::MAX = 20 digits
381    // Prefix is 26 bytes + up to 20 digits = 46 bytes max
382    let mut prefix_buf = [0u8; 64];
383    let prefix_len = {
384        struct SliceWriter<'a> {
385            buf: &'a mut [u8],
386            pos: usize,
387        }
388        impl<'a> Write for SliceWriter<'a> {
389            fn write_str(&mut self, s: &str) -> core::fmt::Result {
390                let bytes = s.as_bytes();
391                let end = self.pos + bytes.len();
392                if end > self.buf.len() {
393                    return Err(core::fmt::Error);
394                }
395                self.buf[self.pos..end].copy_from_slice(bytes);
396                self.pos = end;
397                Ok(())
398            }
399        }
400        let mut w = SliceWriter {
401            buf: &mut prefix_buf,
402            pos: 0,
403        };
404        // write_fmt cannot fail here — buffer is large enough
405        let _ = write!(w, "\x19Ethereum Signed Message:\n{}", message.len());
406        w.pos
407    };
408    let mut hasher = Keccak256::new();
409    hasher.update(&prefix_buf[..prefix_len]);
410    hasher.update(message);
411    let mut hash = [0u8; 32];
412    hash.copy_from_slice(&hasher.finalize());
413    hash
414}
415
416/// **EIP-712** domain separator parameters.
417///
418/// Use [`Eip712Domain::separator`] to compute the 32-byte domain separator hash.
419///
420/// ```no_run
421/// use chains_sdk::ethereum::Eip712Domain;
422///
423/// let contract_addr = [0xCC_u8; 20];
424/// let domain = Eip712Domain {
425///     name: "MyDapp",
426///     version: "1",
427///     chain_id: 1,
428///     verifying_contract: &contract_addr,
429/// };
430/// let sep = domain.separator();
431/// ```
432pub struct Eip712Domain<'a> {
433    /// Human-readable name of the signing domain (e.g., "Uniswap").
434    pub name: &'a str,
435    /// Current major version of the signing domain (e.g., "1").
436    pub version: &'a str,
437    /// EIP-155 chain ID (1 = mainnet, 5 = goerli, etc.).
438    pub chain_id: u64,
439    /// Address of the contract that will verify the signature (20 bytes).
440    pub verifying_contract: &'a [u8; 20],
441}
442
443impl<'a> Eip712Domain<'a> {
444    /// The EIP-712 domain type hash:
445    /// `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`
446    pub fn type_hash() -> [u8; 32] {
447        let mut hash = [0u8; 32];
448        hash.copy_from_slice(&Keccak256::digest(
449            b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
450        ));
451        hash
452    }
453
454    /// Compute the 32-byte domain separator.
455    ///
456    /// `keccak256(abi.encode(TYPE_HASH, keccak256(name), keccak256(version), chainId, verifyingContract))`
457    pub fn separator(&self) -> [u8; 32] {
458        let type_hash = Self::type_hash();
459        let name_hash = Keccak256::digest(self.name.as_bytes());
460        let version_hash = Keccak256::digest(self.version.as_bytes());
461
462        // ABI encode: 5 * 32 bytes = 160 bytes
463        let mut encoded = [0u8; 160];
464        encoded[0..32].copy_from_slice(&type_hash);
465        encoded[32..64].copy_from_slice(&name_hash);
466        encoded[64..96].copy_from_slice(&version_hash);
467
468        // uint256 chainId (big-endian, right-aligned in 32 bytes)
469        encoded[120..128].copy_from_slice(&self.chain_id.to_be_bytes());
470
471        // address verifyingContract (right-aligned in 32 bytes, last 20 bytes)
472        encoded[140..160].copy_from_slice(self.verifying_contract);
473
474        let mut hash = [0u8; 32];
475        hash.copy_from_slice(&Keccak256::digest(encoded));
476        hash
477    }
478}
479
480/// Compute the EIP-712 signing hash:
481/// `keccak256("\x19\x01" || domain_separator || struct_hash)`
482pub fn eip712_hash(domain_separator: &[u8; 32], struct_hash: &[u8; 32]) -> [u8; 32] {
483    let mut payload = [0u8; 66]; // 2 + 32 + 32
484    payload[0] = 0x19;
485    payload[1] = 0x01;
486    payload[2..34].copy_from_slice(domain_separator);
487    payload[34..66].copy_from_slice(struct_hash);
488
489    let mut hash = [0u8; 32];
490    hash.copy_from_slice(&Keccak256::digest(payload));
491    hash
492}
493
494impl traits::Signer for EthereumSigner {
495    type Signature = EthereumSignature;
496    type Error = SignerError;
497
498    fn sign(&self, message: &[u8]) -> Result<EthereumSignature, SignerError> {
499        let digest = Keccak256::digest(message);
500        let mut hash = [0u8; 32];
501        hash.copy_from_slice(&digest);
502        self.sign_digest(&hash)
503    }
504
505    fn sign_prehashed(&self, digest: &[u8]) -> Result<EthereumSignature, SignerError> {
506        if digest.len() != 32 {
507            return Err(SignerError::InvalidHashLength {
508                expected: 32,
509                got: digest.len(),
510            });
511        }
512        let mut hash = [0u8; 32];
513        hash.copy_from_slice(digest);
514        self.sign_digest(&hash)
515    }
516
517    fn public_key_bytes(&self) -> Vec<u8> {
518        self.signing_key
519            .verifying_key()
520            .to_encoded_point(true)
521            .as_bytes()
522            .to_vec()
523    }
524
525    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
526        self.signing_key
527            .verifying_key()
528            .to_encoded_point(false)
529            .as_bytes()
530            .to_vec()
531    }
532}
533
534impl traits::KeyPair for EthereumSigner {
535    fn generate() -> Result<Self, SignerError> {
536        let mut key_bytes = zeroize::Zeroizing::new([0u8; 32]);
537        crate::security::secure_random(&mut *key_bytes)?;
538        let signing_key = SigningKey::from_bytes((&*key_bytes).into())
539            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
540        Ok(Self { signing_key })
541    }
542
543    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
544        if private_key.len() != 32 {
545            return Err(SignerError::InvalidPrivateKey(format!(
546                "expected 32 bytes, got {}",
547                private_key.len()
548            )));
549        }
550        let signing_key = SigningKey::from_bytes(private_key.into())
551            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
552        Ok(Self { signing_key })
553    }
554
555    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
556        Zeroizing::new(self.signing_key.to_bytes().to_vec())
557    }
558}
559
560/// Ethereum ECDSA verifier.
561pub struct EthereumVerifier {
562    verifying_key: VerifyingKey,
563}
564
565impl EthereumVerifier {
566    /// Create a verifier from raw compressed or uncompressed public key bytes.
567    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
568        let verifying_key = VerifyingKey::from_sec1_bytes(bytes)
569            .map_err(|e| SignerError::InvalidPublicKey(e.to_string()))?;
570        Ok(Self { verifying_key })
571    }
572
573    /// Verify against a pre-hashed digest.
574    fn verify_digest(
575        &self,
576        digest: &[u8; 32],
577        signature: &EthereumSignature,
578    ) -> Result<bool, SignerError> {
579        let rec_id = RecoveryId::from_byte(signature.recovery_bit())
580            .ok_or_else(|| SignerError::InvalidSignature("invalid recovery id".into()))?;
581
582        let mut sig_bytes = [0u8; 64];
583        sig_bytes[..32].copy_from_slice(&signature.r);
584        sig_bytes[32..].copy_from_slice(&signature.s);
585
586        let k256_sig = K256Signature::from_bytes((&sig_bytes).into())
587            .map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
588
589        let recovered = VerifyingKey::recover_from_prehash(digest, &k256_sig, rec_id)
590            .map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
591
592        Ok(bool::from(
593            recovered
594                .to_encoded_point(true)
595                .as_bytes()
596                .ct_eq(self.verifying_key.to_encoded_point(true).as_bytes()),
597        ))
598    }
599}
600
601impl traits::Verifier for EthereumVerifier {
602    type Signature = EthereumSignature;
603    type Error = SignerError;
604
605    fn verify(&self, message: &[u8], signature: &EthereumSignature) -> Result<bool, SignerError> {
606        let digest = Keccak256::digest(message);
607        let mut hash = [0u8; 32];
608        hash.copy_from_slice(&digest);
609        self.verify_digest(&hash, signature)
610    }
611
612    fn verify_prehashed(
613        &self,
614        digest: &[u8],
615        signature: &EthereumSignature,
616    ) -> Result<bool, SignerError> {
617        if digest.len() != 32 {
618            return Err(SignerError::InvalidHashLength {
619                expected: 32,
620                got: digest.len(),
621            });
622        }
623        let mut hash = [0u8; 32];
624        hash.copy_from_slice(digest);
625        self.verify_digest(&hash, signature)
626    }
627}
628
629impl EthereumVerifier {
630    /// **EIP-712**: Verify a typed data signature.
631    ///
632    /// Recomputes `keccak256("\x19\x01" || domain_separator || struct_hash)` and verifies.
633    pub fn verify_typed_data(
634        &self,
635        domain_separator: &[u8; 32],
636        struct_hash: &[u8; 32],
637        signature: &EthereumSignature,
638    ) -> Result<bool, SignerError> {
639        let digest = eip712_hash(domain_separator, struct_hash);
640        self.verify_digest(&digest, signature)
641    }
642
643    /// **EIP-191**: Verify a personal message signature.
644    ///
645    /// Recomputes `keccak256("\x19Ethereum Signed Message:\n{len}{message}")` and verifies.
646    pub fn verify_personal_sign(
647        &self,
648        message: &[u8],
649        signature: &EthereumSignature,
650    ) -> Result<bool, SignerError> {
651        let digest = eip191_hash(message);
652        self.verify_digest(&digest, signature)
653    }
654}
655
656#[cfg(test)]
657#[allow(clippy::unwrap_used, clippy::expect_used)]
658mod tests {
659    use super::*;
660    use crate::traits::{KeyPair, Signer, Verifier};
661
662    #[test]
663    fn test_generate_keypair() {
664        let signer = EthereumSigner::generate().unwrap();
665        let pubkey = signer.public_key_bytes();
666        assert_eq!(pubkey.len(), 33); // compressed
667    }
668
669    #[test]
670    fn test_from_bytes_roundtrip() {
671        let signer = EthereumSigner::generate().unwrap();
672        let key_bytes = signer.private_key_bytes();
673        let restored = EthereumSigner::from_bytes(&key_bytes).unwrap();
674        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
675    }
676
677    #[test]
678    fn test_sign_verify_roundtrip() {
679        let signer = EthereumSigner::generate().unwrap();
680        let msg = b"hello ethereum";
681        let sig = signer.sign(msg).unwrap();
682        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
683        assert!(verifier.verify(msg, &sig).unwrap());
684    }
685
686    #[test]
687    fn test_keccak256_hash() {
688        let hash = Keccak256::digest(b"hello");
689        assert_eq!(
690            hex::encode(hash),
691            "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"
692        );
693    }
694
695    #[test]
696    fn test_low_s_enforcement() {
697        // Sign many messages and verify S is always <= N/2
698        use k256::elliptic_curve::Curve;
699        let signer = EthereumSigner::generate().unwrap();
700        let order = k256::Secp256k1::ORDER;
701        let half_n = order.shr_vartime(1);
702
703        for i in 0u32..50 {
704            let msg = format!("test message {}", i);
705            let sig = signer.sign(msg.as_bytes()).unwrap();
706            let s = k256::U256::from_be_slice(&sig.s);
707            assert!(s <= half_n, "S value not low-S normalized");
708        }
709    }
710
711    #[test]
712    fn test_recovery_id() {
713        let signer = EthereumSigner::generate().unwrap();
714        let sig = signer.sign(b"test recovery").unwrap();
715        assert!(sig.v == 27 || sig.v == 28);
716    }
717
718    #[test]
719    fn test_address_derivation() {
720        // Known test vector: private key -> Ethereum address
721        let privkey =
722            hex::decode("4c0883a69102937d6231471b5dbb6204fe512961708279f3c6f2b54729a0f29e")
723                .unwrap();
724        let signer = EthereumSigner::from_bytes(&privkey).unwrap();
725        let addr = signer.address();
726        assert_eq!(
727            hex::encode(addr).to_lowercase(),
728            "0d77521fa96e4c41e4190cab2dbe0d613c4afa9d"
729        );
730    }
731
732    #[test]
733    fn test_known_vector_eth() {
734        // Known private key -> sign "hello" -> verify passes
735        let privkey =
736            hex::decode("4c0883a69102937d6231471b5dbb6204fe512961708279f3c6f2b54729a0f29e")
737                .unwrap();
738        let signer = EthereumSigner::from_bytes(&privkey).unwrap();
739        let sig = signer.sign(b"hello").unwrap();
740        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
741        assert!(verifier.verify(b"hello", &sig).unwrap());
742        // v must be 27 or 28
743        assert!(sig.v == 27 || sig.v == 28);
744    }
745
746    #[test]
747    fn test_invalid_privkey_rejected() {
748        // All zeros
749        assert!(EthereumSigner::from_bytes(&[0u8; 32]).is_err());
750        // Too short
751        assert!(EthereumSigner::from_bytes(&[1u8; 31]).is_err());
752        // Too long
753        assert!(EthereumSigner::from_bytes(&[1u8; 33]).is_err());
754    }
755
756    #[test]
757    fn test_tampered_sig_fails() {
758        let signer = EthereumSigner::generate().unwrap();
759        let sig = signer.sign(b"test tamper").unwrap();
760        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
761
762        // Flip a byte in r
763        let mut tampered = sig.clone();
764        tampered.r[0] ^= 0xff;
765        // Tampered signature should either fail verification or return false
766        let result = verifier.verify(b"test tamper", &tampered);
767        assert!(result.is_err() || !result.unwrap());
768    }
769
770    #[test]
771    fn test_wrong_pubkey_fails() {
772        let signer1 = EthereumSigner::generate().unwrap();
773        let signer2 = EthereumSigner::generate().unwrap();
774        let sig = signer1.sign(b"test wrong key").unwrap();
775        let verifier =
776            EthereumVerifier::from_public_key_bytes(&signer2.public_key_bytes()).unwrap();
777        let result = verifier.verify(b"test wrong key", &sig).unwrap();
778        assert!(!result);
779    }
780
781    #[test]
782    fn test_empty_message() {
783        let signer = EthereumSigner::generate().unwrap();
784        let sig = signer.sign(b"").unwrap();
785        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
786        assert!(verifier.verify(b"", &sig).unwrap());
787    }
788
789    #[test]
790    fn test_large_message() {
791        let signer = EthereumSigner::generate().unwrap();
792        let msg = vec![0xab_u8; 1_000_000]; // 1 MB
793        let sig = signer.sign(&msg).unwrap();
794        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
795        assert!(verifier.verify(&msg, &sig).unwrap());
796    }
797
798    #[test]
799    fn test_sign_prehashed_roundtrip() {
800        let signer = EthereumSigner::generate().unwrap();
801        let msg = b"prehash test";
802        let digest = Keccak256::digest(msg);
803
804        let sig_raw = signer.sign(msg).unwrap();
805        let sig_pre = signer.sign_prehashed(&digest).unwrap();
806
807        // Both should verify
808        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
809        assert!(verifier.verify(msg, &sig_raw).unwrap());
810        assert!(verifier.verify_prehashed(&digest, &sig_pre).unwrap());
811    }
812
813    #[test]
814    fn test_verify_prehashed() {
815        let signer = EthereumSigner::generate().unwrap();
816        let msg = b"verify prehash";
817        let digest = Keccak256::digest(msg);
818        let sig = signer.sign(msg).unwrap();
819        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
820        assert!(verifier.verify_prehashed(&digest, &sig).unwrap());
821    }
822
823    #[test]
824    fn test_zeroize_on_drop() {
825        let signer = EthereumSigner::generate().unwrap();
826        let key_bytes = signer.private_key_bytes();
827        // Verify the type is Zeroizing (compile-time guarantee)
828        let _: Zeroizing<Vec<u8>> = key_bytes;
829        // Drop and generate fresh — verifying the API contract
830        drop(signer);
831        let fresh = EthereumSigner::generate().unwrap();
832        let _: Zeroizing<Vec<u8>> = fresh.private_key_bytes();
833    }
834
835    #[test]
836    fn test_signature_bytes_roundtrip() {
837        let signer = EthereumSigner::generate().unwrap();
838        let sig = signer.sign(b"roundtrip").unwrap();
839        let bytes = sig.to_bytes();
840        let restored = EthereumSignature::from_bytes(&bytes).unwrap();
841        assert_eq!(sig.r, restored.r);
842        assert_eq!(sig.s, restored.s);
843        assert_eq!(sig.v, restored.v);
844    }
845
846    // ─── EIP-712 Tests ──────────────────────────────────────────────────
847
848    #[test]
849    fn test_eip712_domain_type_hash() {
850        let type_hash = Eip712Domain::type_hash();
851        // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
852        let expected = Keccak256::digest(
853            b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
854        );
855        assert_eq!(type_hash[..], expected[..]);
856    }
857
858    #[test]
859    fn test_eip712_domain_separator() {
860        let contract_addr: [u8; 20] = [0xCC; 20];
861        let domain = Eip712Domain {
862            name: "TestDapp",
863            version: "1",
864            chain_id: 1,
865            verifying_contract: &contract_addr,
866        };
867        let sep = domain.separator();
868        // Domain separator must be 32 bytes and deterministic
869        assert_eq!(sep.len(), 32);
870        let sep2 = domain.separator();
871        assert_eq!(sep, sep2);
872    }
873
874    #[test]
875    fn test_eip712_hash_prefix() {
876        let domain_sep = [0xAA_u8; 32];
877        let struct_hash = [0xBB_u8; 32];
878        let hash = eip712_hash(&domain_sep, &struct_hash);
879        // The hash must be 32 bytes and different from just keccak256(struct_hash)
880        assert_eq!(hash.len(), 32);
881        let plain_hash = Keccak256::digest(struct_hash);
882        assert_ne!(&hash[..], &plain_hash[..]);
883    }
884
885    #[test]
886    fn test_eip712_sign_verify_roundtrip() {
887        let signer = EthereumSigner::generate().unwrap();
888        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
889
890        let contract_addr: [u8; 20] = [0xCC; 20];
891        let domain = Eip712Domain {
892            name: "MyDapp",
893            version: "1",
894            chain_id: 1,
895            verifying_contract: &contract_addr,
896        };
897        let domain_sep = domain.separator();
898
899        // Simulate a struct hash (e.g., keccak256 of an encoded Permit struct)
900        let struct_hash: [u8; 32] = {
901            let mut h = [0u8; 32];
902            h.copy_from_slice(&Keccak256::digest(b"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"));
903            h
904        };
905
906        let sig = signer.sign_typed_data(&domain_sep, &struct_hash).unwrap();
907        assert!(sig.v == 27 || sig.v == 28);
908
909        // Verify
910        assert!(verifier
911            .verify_typed_data(&domain_sep, &struct_hash, &sig)
912            .unwrap());
913    }
914
915    #[test]
916    fn test_eip712_wrong_domain_fails() {
917        let signer = EthereumSigner::generate().unwrap();
918        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
919
920        let struct_hash = [0xBB_u8; 32];
921        let domain_sep = [0xAA_u8; 32];
922        let sig = signer.sign_typed_data(&domain_sep, &struct_hash).unwrap();
923
924        // Verify with wrong domain must fail
925        let wrong_domain = [0xFF_u8; 32];
926        let result = verifier
927            .verify_typed_data(&wrong_domain, &struct_hash, &sig)
928            .unwrap();
929        assert!(!result);
930    }
931
932    // ─── EIP-191 Tests ──────────────────────────────────────────────────
933
934    #[test]
935    fn test_eip191_sign_verify_roundtrip() {
936        let signer = EthereumSigner::generate().unwrap();
937        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
938        let msg = b"Hello from chains-sdk!";
939        let sig = signer.personal_sign(msg).unwrap();
940        assert!(sig.v == 27 || sig.v == 28);
941        assert!(verifier.verify_personal_sign(msg, &sig).unwrap());
942    }
943
944    #[test]
945    fn test_eip191_hash_known_vector() {
946        // keccak256("\x19Ethereum Signed Message:\n5hello")
947        let hash = eip191_hash(b"hello");
948        let expected = Keccak256::digest(b"\x19Ethereum Signed Message:\n5hello");
949        assert_eq!(&hash[..], &expected[..]);
950    }
951
952    #[test]
953    fn test_eip191_wrong_message_fails() {
954        let signer = EthereumSigner::generate().unwrap();
955        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
956        let sig = signer.personal_sign(b"correct message").unwrap();
957        let result = verifier.verify_personal_sign(b"wrong message", &sig);
958        assert!(result.is_err() || !result.unwrap());
959    }
960
961    #[test]
962    fn test_eip191_differs_from_raw_sign() {
963        let signer = EthereumSigner::generate().unwrap();
964        let verifier = EthereumVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
965        let msg = b"test";
966        let raw_sig = signer.sign(msg).unwrap();
967        let personal_sig = signer.personal_sign(msg).unwrap();
968        // They must produce different signatures (different hash)
969        assert_ne!(raw_sig.r, personal_sig.r);
970        // personal_sign signature should NOT verify via raw verify
971        let result = verifier.verify(msg, &personal_sig).unwrap();
972        assert!(!result);
973    }
974}