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