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