1pub 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[must_use]
38pub struct EthereumSignature {
39 pub r: [u8; 32],
41 pub s: [u8; 32],
43 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 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 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 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 pub fn recovery_bit(&self) -> Result<u8, SignerError> {
111 if self.v >= 35 {
112 if self.v <= 36 {
114 return Err(SignerError::InvalidSignature(format!(
115 "non-canonical EIP-155 v value {}",
116 self.v
117 )));
118 }
119 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
134pub struct EthereumSigner {
139 signing_key: SigningKey,
140}
141
142impl Drop for EthereumSigner {
143 fn drop(&mut self) {
144 }
146}
147
148impl EthereumSigner {
149 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..]; let hash = Keccak256::digest(pubkey_bytes);
156 let mut addr = [0u8; 20];
157 addr.copy_from_slice(&hash[12..]);
158 addr
159 }
160
161 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 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 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 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 pub fn personal_sign(&self, message: &[u8]) -> Result<EthereumSignature, SignerError> {
211 let digest = eip191_hash(message);
212 self.sign_digest(&digest)
213 }
214
215 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 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 let recovery_bit = sig.v - 27; 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 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 pub fn address_checksum(&self) -> String {
269 eip55_checksum(&self.address())
270 }
271
272 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
307pub 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
331pub 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 if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
343 return false;
344 }
345 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; }
351 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
372pub 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
383pub 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
413pub fn eip191_hash(message: &[u8]) -> [u8; 32] {
418 use core::fmt::Write;
419 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 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
455pub struct Eip712Domain<'a> {
472 pub name: &'a str,
474 pub version: &'a str,
476 pub chain_id: u64,
478 pub verifying_contract: &'a [u8; 20],
480}
481
482impl<'a> Eip712Domain<'a> {
483 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 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 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 encoded[120..128].copy_from_slice(&self.chain_id.to_be_bytes());
509
510 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
519pub fn eip712_hash(domain_separator: &[u8; 32], struct_hash: &[u8; 32]) -> [u8; 32] {
522 let mut payload = [0u8; 66]; 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
533pub(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
608pub struct EthereumVerifier {
610 verifying_key: VerifyingKey,
611}
612
613impl EthereumVerifier {
614 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 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 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 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); }
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 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 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 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 assert!(sig.v == 27 || sig.v == 28);
797 }
798
799 #[test]
800 fn test_invalid_privkey_rejected() {
801 assert!(EthereumSigner::from_bytes(&[0u8; 32]).is_err());
803 assert!(EthereumSigner::from_bytes(&[1u8; 31]).is_err());
805 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 let mut tampered = sig.clone();
817 tampered.r[0] ^= 0xff;
818 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 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 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]; 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 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 let _: Zeroizing<Vec<u8>> = key_bytes;
913 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 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 #[test]
973 fn test_eip712_domain_type_hash() {
974 let type_hash = Eip712Domain::type_hash();
975 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 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 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 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 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 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 #[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 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 assert_ne!(raw_sig.r, personal_sig.r);
1094 let result = verifier.verify(msg, &personal_sig).unwrap();
1096 assert!(!result);
1097 }
1098}