txgate_crypto/signer.rs
1//! High-level signing traits and implementations.
2//!
3//! This module provides the [`Signer`] trait for abstracting over different
4//! signing implementations, and concrete implementations for specific curves.
5//!
6//! # Supported Signers
7//!
8//! - [`Secp256k1Signer`] - For Ethereum, Bitcoin, Tron, and Ripple
9//! - [`Ed25519Signer`] - For Solana and other ed25519-based chains
10//!
11//! # Example
12//!
13//! ```rust
14//! use txgate_crypto::signer::{Signer, Secp256k1Signer, Chain};
15//!
16//! // Generate a new signer
17//! let signer = Secp256k1Signer::generate();
18//!
19//! // Get the Ethereum address
20//! let address = signer.address(Chain::Ethereum).expect("valid address");
21//! println!("Ethereum address: {address}");
22//!
23//! // Sign a message hash
24//! let hash = [0u8; 32]; // In practice, this would be a real hash
25//! let signature = signer.sign(&hash).expect("signing failed");
26//! assert_eq!(signature.len(), 65); // r || s || v
27//! ```
28
29use bitcoin::secp256k1::PublicKey as BitcoinPublicKey;
30use bitcoin::{Address, CompressedPublicKey, Network};
31use sha3::{Digest, Keccak256};
32
33use crate::keypair::{KeyPair, Secp256k1KeyPair};
34use txgate_core::error::SignError;
35
36// ============================================================================
37// Chain Enum
38// ============================================================================
39
40/// Supported blockchain networks for address derivation.
41///
42/// This enum represents the different blockchain networks that `TxGate` supports
43/// for address derivation. Each chain may have different address formats and
44/// derivation rules.
45///
46/// # Example
47///
48/// ```rust
49/// use txgate_crypto::signer::{Signer, Secp256k1Signer, Chain};
50///
51/// let signer = Secp256k1Signer::generate();
52///
53/// // Derive Ethereum address
54/// let eth_addr = signer.address(Chain::Ethereum).expect("valid");
55/// assert!(eth_addr.starts_with("0x"));
56///
57/// // Solana requires Ed25519, so this will fail
58/// let sol_result = signer.address(Chain::Solana);
59/// assert!(sol_result.is_err());
60/// ```
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum Chain {
63 /// Ethereum and EVM-compatible chains (Polygon, BSC, Arbitrum, etc.)
64 Ethereum,
65 /// Bitcoin mainnet
66 Bitcoin,
67 /// Solana (requires Ed25519)
68 Solana,
69 /// Tron network (uses same curve as Ethereum but different address format)
70 Tron,
71 /// Ripple/XRP Ledger
72 Ripple,
73}
74
75impl std::fmt::Display for Chain {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::Ethereum => write!(f, "ethereum"),
79 Self::Bitcoin => write!(f, "bitcoin"),
80 Self::Solana => write!(f, "solana"),
81 Self::Tron => write!(f, "tron"),
82 Self::Ripple => write!(f, "ripple"),
83 }
84 }
85}
86
87// ============================================================================
88// CurveType Enum
89// ============================================================================
90
91/// Elliptic curve types supported by `TxGate`.
92///
93/// This enum identifies the cryptographic curve used by a signer,
94/// which is important for ensuring compatibility with different blockchains.
95///
96/// # Curve Selection
97///
98/// - [`CurveType::Secp256k1`] - Used by Ethereum, Bitcoin, Tron, Ripple
99/// - [`CurveType::Ed25519`] - Used by Solana, NEAR, Cosmos (some chains)
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub enum CurveType {
102 /// The secp256k1 curve used by Bitcoin, Ethereum, and related chains.
103 Secp256k1,
104 /// The Ed25519 curve used by Solana, NEAR, and some other chains.
105 Ed25519,
106}
107
108impl std::fmt::Display for CurveType {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 Self::Secp256k1 => write!(f, "secp256k1"),
112 Self::Ed25519 => write!(f, "ed25519"),
113 }
114 }
115}
116
117// ============================================================================
118// Signer Trait
119// ============================================================================
120
121/// Trait for transaction signing operations.
122///
123/// This trait provides a high-level interface for signing transaction hashes
124/// and deriving blockchain addresses from the underlying key pair.
125///
126/// # Thread Safety
127///
128/// All implementations must be `Send + Sync` to support multi-threaded
129/// signing operations.
130///
131/// # Signature Format
132///
133/// For secp256k1 signers, the signature is returned as 65 bytes:
134/// - `r` (32 bytes) - The R component of the ECDSA signature
135/// - `s` (32 bytes) - The S component of the ECDSA signature
136/// - `v` (1 byte) - The recovery ID (0 or 1)
137///
138/// For Ethereum transactions, you may need to adjust `v`:
139/// - Legacy transactions: `v = recovery_id + 27`
140/// - EIP-155: `v = recovery_id + 35 + chain_id * 2`
141/// - EIP-2930/EIP-1559: `v = recovery_id`
142///
143/// # Example
144///
145/// ```rust
146/// use txgate_crypto::signer::{Signer, Secp256k1Signer, Chain, CurveType};
147///
148/// fn sign_transaction<S: Signer>(signer: &S, hash: &[u8; 32]) -> Vec<u8> {
149/// signer.sign(hash).expect("signing failed")
150/// }
151///
152/// let signer = Secp256k1Signer::generate();
153/// assert_eq!(signer.curve(), CurveType::Secp256k1);
154///
155/// let hash = [0u8; 32];
156/// let sig = sign_transaction(&signer, &hash);
157/// assert_eq!(sig.len(), 65);
158/// ```
159pub trait Signer: Send + Sync {
160 /// Sign a 32-byte message hash.
161 ///
162 /// # Arguments
163 /// * `hash` - The pre-computed hash of the message/transaction to sign.
164 /// This should be a cryptographic hash (e.g., Keccak-256 for Ethereum),
165 /// NOT the raw message.
166 ///
167 /// # Returns
168 /// A signature that can be verified against this signer's public key.
169 /// For secp256k1, this is 65 bytes: `r || s || v` where `v` is the recovery ID.
170 ///
171 /// # Errors
172 /// Returns an error if signing fails (e.g., due to key corruption).
173 ///
174 /// # Example
175 ///
176 /// ```rust
177 /// use txgate_crypto::signer::{Signer, Secp256k1Signer};
178 /// use sha3::{Digest, Keccak256};
179 ///
180 /// let signer = Secp256k1Signer::generate();
181 ///
182 /// // Hash the message first
183 /// let message = b"Hello, Ethereum!";
184 /// let hash: [u8; 32] = Keccak256::digest(message).into();
185 ///
186 /// // Sign the hash
187 /// let signature = signer.sign(&hash).expect("signing failed");
188 /// assert_eq!(signature.len(), 65);
189 /// ```
190 fn sign(&self, hash: &[u8; 32]) -> Result<Vec<u8>, SignError>;
191
192 /// Get the public key bytes.
193 ///
194 /// Returns the compressed public key:
195 /// - secp256k1: 33 bytes (prefix + X coordinate)
196 /// - ed25519: 32 bytes
197 ///
198 /// # Example
199 ///
200 /// ```rust
201 /// use txgate_crypto::signer::{Signer, Secp256k1Signer};
202 ///
203 /// let signer = Secp256k1Signer::generate();
204 /// let pubkey = signer.public_key();
205 /// assert_eq!(pubkey.len(), 33); // Compressed secp256k1
206 /// ```
207 fn public_key(&self) -> &[u8];
208
209 /// Derive the blockchain address for a specific chain.
210 ///
211 /// # Arguments
212 /// * `chain` - The blockchain network to derive the address for.
213 ///
214 /// # Returns
215 /// The address as a string with the appropriate format for the chain:
216 /// - Ethereum: EIP-55 checksummed hex with `0x` prefix
217 /// - Bitcoin: `Base58Check` encoded P2PKH address
218 /// - Tron: `Base58Check` encoded with `0x41` prefix (starts with `T`)
219 /// - Solana: Base58 encoded (requires Ed25519)
220 /// - Ripple: `Base58Check` with Ripple alphabet
221 ///
222 /// # Errors
223 /// Returns an error if:
224 /// - The chain requires a different curve (e.g., Solana needs Ed25519)
225 /// - Address derivation is not yet implemented for the chain
226 ///
227 /// # Example
228 ///
229 /// ```rust
230 /// use txgate_crypto::signer::{Signer, Secp256k1Signer, Chain};
231 ///
232 /// let signer = Secp256k1Signer::generate();
233 ///
234 /// // Get Ethereum address (EIP-55 checksummed)
235 /// let address = signer.address(Chain::Ethereum).expect("valid");
236 /// assert!(address.starts_with("0x"));
237 /// assert_eq!(address.len(), 42); // 0x + 40 hex chars
238 /// ```
239 fn address(&self, chain: Chain) -> Result<String, SignError>;
240
241 /// Get the curve type used by this signer.
242 ///
243 /// This is useful for determining compatibility with different chains.
244 ///
245 /// # Example
246 ///
247 /// ```rust
248 /// use txgate_crypto::signer::{Signer, Secp256k1Signer, CurveType};
249 ///
250 /// let signer = Secp256k1Signer::generate();
251 /// assert_eq!(signer.curve(), CurveType::Secp256k1);
252 /// ```
253 fn curve(&self) -> CurveType;
254}
255
256// ============================================================================
257// Secp256k1Signer Implementation
258// ============================================================================
259
260/// Secp256k1 signer for Ethereum, Bitcoin, Tron, and Ripple.
261///
262/// This signer wraps a [`Secp256k1KeyPair`] and provides a high-level
263/// signing interface that returns recoverable signatures suitable for
264/// blockchain transactions.
265///
266/// # Signature Format
267///
268/// Signatures are returned as 65 bytes: `r (32) || s (32) || v (1)`
269/// where `v` is the raw recovery ID (0 or 1).
270///
271/// # Security
272///
273/// - The underlying key pair uses secure key material handling
274/// - Signatures are normalized to prevent malleability
275/// - Recovery IDs are computed correctly for `ecrecover`
276///
277/// # Example
278///
279/// ```rust
280/// use txgate_crypto::signer::{Signer, Secp256k1Signer, Chain};
281///
282/// // Generate a new signer
283/// let signer = Secp256k1Signer::generate();
284///
285/// // Or create from raw bytes
286/// let secret = [0x42u8; 32]; // Use real secret in production!
287/// let signer = Secp256k1Signer::from_bytes(secret).expect("valid key");
288///
289/// // Get the Ethereum address
290/// let address = signer.address(Chain::Ethereum).expect("valid");
291/// println!("Address: {address}");
292///
293/// // Sign a hash
294/// let hash = [0u8; 32];
295/// let signature = signer.sign(&hash).expect("signing failed");
296/// assert_eq!(signature.len(), 65);
297/// ```
298#[derive(Debug)]
299pub struct Secp256k1Signer {
300 /// The underlying key pair
301 key_pair: Secp256k1KeyPair,
302 /// Cached compressed public key bytes
303 public_key_bytes: [u8; 33],
304}
305
306impl Secp256k1Signer {
307 /// Create a new signer from a key pair.
308 ///
309 /// # Arguments
310 /// * `key_pair` - The secp256k1 key pair to use for signing.
311 ///
312 /// # Example
313 ///
314 /// ```rust
315 /// use txgate_crypto::keypair::{KeyPair, Secp256k1KeyPair};
316 /// use txgate_crypto::signer::Secp256k1Signer;
317 ///
318 /// let key_pair = Secp256k1KeyPair::generate();
319 /// let signer = Secp256k1Signer::new(key_pair);
320 /// ```
321 #[must_use]
322 pub fn new(key_pair: Secp256k1KeyPair) -> Self {
323 let public_key_bytes = *key_pair.public_key().compressed();
324 Self {
325 key_pair,
326 public_key_bytes,
327 }
328 }
329
330 /// Create a new signer with a randomly generated key.
331 ///
332 /// Uses a cryptographically secure random number generator.
333 ///
334 /// # Example
335 ///
336 /// ```rust
337 /// use txgate_crypto::signer::Secp256k1Signer;
338 ///
339 /// let signer = Secp256k1Signer::generate();
340 /// ```
341 #[must_use]
342 pub fn generate() -> Self {
343 Self::new(Secp256k1KeyPair::generate())
344 }
345
346 /// Create a signer from raw secret key bytes.
347 ///
348 /// # Arguments
349 /// * `bytes` - The 32-byte secret key material.
350 ///
351 /// # Errors
352 /// Returns an error if the bytes don't represent a valid secp256k1
353 /// secret key (e.g., zero or greater than the curve order).
354 ///
355 /// # Example
356 ///
357 /// ```rust
358 /// use txgate_crypto::signer::Secp256k1Signer;
359 ///
360 /// let secret = [0x42u8; 32];
361 /// let signer = Secp256k1Signer::from_bytes(secret).expect("valid key");
362 /// ```
363 pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, SignError> {
364 let key_pair = Secp256k1KeyPair::from_bytes(bytes)?;
365 Ok(Self::new(key_pair))
366 }
367
368 /// Get a reference to the underlying key pair.
369 ///
370 /// This is useful when you need access to the full key pair functionality.
371 #[must_use]
372 pub const fn key_pair(&self) -> &Secp256k1KeyPair {
373 &self.key_pair
374 }
375
376 /// Derive the Ethereum address and return as EIP-55 checksummed string.
377 fn ethereum_address(&self) -> String {
378 let address = self.key_pair.public_key().ethereum_address();
379 to_eip55_checksum(&address)
380 }
381
382 /// Derive the Bitcoin P2WPKH (bech32) address for the specified network.
383 ///
384 /// P2WPKH addresses start with `bc1q` on mainnet and `tb1q` on testnet.
385 ///
386 /// # Arguments
387 /// * `network` - The Bitcoin network (mainnet, testnet, signet, regtest).
388 ///
389 /// # Returns
390 /// The bech32-encoded P2WPKH address.
391 fn bitcoin_p2wpkh_address(&self, network: Network) -> Result<String, SignError> {
392 // Get the compressed public key bytes (33 bytes)
393 let compressed = self.public_key_bytes;
394
395 // Create a bitcoin PublicKey from the compressed bytes
396 let bitcoin_pubkey = BitcoinPublicKey::from_slice(&compressed)
397 .map_err(|e| SignError::signature_failed(format!("Invalid public key: {e}")))?;
398
399 // Create CompressedPublicKey wrapper
400 let compressed_pubkey = CompressedPublicKey(bitcoin_pubkey);
401
402 // Create P2WPKH address
403 let address = Address::p2wpkh(&compressed_pubkey, network);
404
405 Ok(address.to_string())
406 }
407}
408
409impl Signer for Secp256k1Signer {
410 fn sign(&self, hash: &[u8; 32]) -> Result<Vec<u8>, SignError> {
411 let signature = self.key_pair.sign(hash)?;
412 // Return 65-byte recoverable signature: r || s || v
413 Ok(signature.to_recoverable_bytes().to_vec())
414 }
415
416 fn public_key(&self) -> &[u8] {
417 &self.public_key_bytes
418 }
419
420 fn address(&self, chain: Chain) -> Result<String, SignError> {
421 match chain {
422 Chain::Ethereum => Ok(self.ethereum_address()),
423 Chain::Bitcoin => {
424 // P2WPKH bech32 address (starts with bc1q)
425 self.bitcoin_p2wpkh_address(Network::Bitcoin)
426 }
427 Chain::Tron => {
428 // TRON uses same format as Ethereum but with T prefix and Base58Check
429 // TODO: Implement Tron address derivation
430 Err(SignError::signature_failed(
431 "Tron address derivation not yet implemented",
432 ))
433 }
434 Chain::Ripple => {
435 // Ripple uses secp256k1 but with Base58Check using Ripple alphabet
436 // TODO: Implement Ripple address derivation
437 Err(SignError::signature_failed(
438 "Ripple address derivation not yet implemented",
439 ))
440 }
441 Chain::Solana => {
442 // Solana requires Ed25519, not secp256k1
443 Err(SignError::wrong_curve("ed25519", "secp256k1"))
444 }
445 }
446 }
447
448 fn curve(&self) -> CurveType {
449 CurveType::Secp256k1
450 }
451}
452
453// ============================================================================
454// Ed25519Signer Implementation
455// ============================================================================
456
457/// Ed25519 signer for Solana and other ed25519-based chains.
458///
459/// This signer wraps an [`Ed25519KeyPair`](crate::keypair::Ed25519KeyPair) and provides
460/// a high-level signing interface suitable for blockchain transactions.
461///
462/// # Signature Format
463///
464/// Signatures are returned as 64 bytes: the standard ed25519 signature format.
465///
466/// # Security
467///
468/// - The underlying key pair uses secure key material handling
469/// - Uses ed25519-dalek for cryptographic operations
470///
471/// # Example
472///
473/// ```rust
474/// use txgate_crypto::signer::{Signer, Ed25519Signer, Chain};
475///
476/// // Generate a new signer
477/// let signer = Ed25519Signer::generate();
478///
479/// // Get the Solana address
480/// let address = signer.address(Chain::Solana).expect("valid");
481/// println!("Solana address: {address}");
482///
483/// // Sign a hash
484/// let hash = [0u8; 32];
485/// let signature = signer.sign(&hash).expect("signing failed");
486/// assert_eq!(signature.len(), 64);
487/// ```
488#[derive(Debug)]
489pub struct Ed25519Signer {
490 /// The underlying key pair
491 key_pair: crate::keypair::Ed25519KeyPair,
492 /// Cached public key bytes
493 public_key_bytes: [u8; 32],
494}
495
496impl Ed25519Signer {
497 /// Create a new signer from a key pair.
498 ///
499 /// # Arguments
500 /// * `key_pair` - The ed25519 key pair to use for signing.
501 ///
502 /// # Example
503 ///
504 /// ```rust
505 /// use txgate_crypto::keypair::{KeyPair, Ed25519KeyPair};
506 /// use txgate_crypto::signer::Ed25519Signer;
507 ///
508 /// let key_pair = Ed25519KeyPair::generate();
509 /// let signer = Ed25519Signer::new(key_pair);
510 /// ```
511 #[must_use]
512 pub fn new(key_pair: crate::keypair::Ed25519KeyPair) -> Self {
513 let public_key_bytes = *key_pair.public_key().as_bytes();
514 Self {
515 key_pair,
516 public_key_bytes,
517 }
518 }
519
520 /// Create a new signer with a randomly generated key.
521 ///
522 /// Uses a cryptographically secure random number generator.
523 ///
524 /// # Example
525 ///
526 /// ```rust
527 /// use txgate_crypto::signer::Ed25519Signer;
528 ///
529 /// let signer = Ed25519Signer::generate();
530 /// ```
531 #[must_use]
532 pub fn generate() -> Self {
533 Self::new(crate::keypair::Ed25519KeyPair::generate())
534 }
535
536 /// Create a signer from raw secret key bytes.
537 ///
538 /// # Arguments
539 /// * `bytes` - The 32-byte secret key material.
540 ///
541 /// # Errors
542 /// Returns an error if the bytes don't represent a valid ed25519 secret key.
543 ///
544 /// # Example
545 ///
546 /// ```rust
547 /// use txgate_crypto::signer::Ed25519Signer;
548 ///
549 /// let secret = [0x42u8; 32];
550 /// let signer = Ed25519Signer::from_bytes(secret).expect("valid key");
551 /// ```
552 pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, SignError> {
553 let key_pair = crate::keypair::Ed25519KeyPair::from_bytes(bytes)?;
554 Ok(Self::new(key_pair))
555 }
556
557 /// Get a reference to the underlying key pair.
558 ///
559 /// This is useful when you need access to the full key pair functionality.
560 #[must_use]
561 pub const fn key_pair(&self) -> &crate::keypair::Ed25519KeyPair {
562 &self.key_pair
563 }
564
565 /// Derive the Solana address (base58-encoded public key).
566 fn solana_address(&self) -> String {
567 self.key_pair.public_key().solana_address()
568 }
569}
570
571impl Signer for Ed25519Signer {
572 fn sign(&self, hash: &[u8; 32]) -> Result<Vec<u8>, SignError> {
573 let signature = self.key_pair.sign(hash)?;
574 // Return 64-byte ed25519 signature
575 Ok(signature.as_ref().to_vec())
576 }
577
578 fn public_key(&self) -> &[u8] {
579 &self.public_key_bytes
580 }
581
582 fn address(&self, chain: Chain) -> Result<String, SignError> {
583 match chain {
584 Chain::Solana => Ok(self.solana_address()),
585 Chain::Ethereum | Chain::Bitcoin | Chain::Tron | Chain::Ripple => {
586 // These chains require secp256k1, not ed25519
587 Err(SignError::wrong_curve("secp256k1", "ed25519"))
588 }
589 }
590 }
591
592 fn curve(&self) -> CurveType {
593 CurveType::Ed25519
594 }
595}
596
597// ============================================================================
598// EIP-55 Address Checksum
599// ============================================================================
600
601/// Convert an Ethereum address to EIP-55 checksummed format.
602///
603/// EIP-55 uses a mixed-case hexadecimal encoding where the case of each
604/// letter encodes a checksum. This helps prevent typos when copying addresses.
605///
606/// # Algorithm
607///
608/// 1. Convert the address to lowercase hex (without `0x` prefix)
609/// 2. Hash the hex string with Keccak-256
610/// 3. For each character in the hex address:
611/// - If it's a digit (0-9), keep it as-is
612/// - If it's a letter (a-f), uppercase it if the corresponding nibble
613/// in the hash is >= 8
614///
615/// # Arguments
616/// * `address` - The 20-byte Ethereum address.
617///
618/// # Returns
619/// The EIP-55 checksummed address string with `0x` prefix.
620///
621/// # Example
622///
623/// ```ignore
624/// let address = hex::decode("5aaeb6053f3e94c9b9a09f33669435e7ef1beaed").unwrap();
625/// let checksummed = to_eip55_checksum(&address.try_into().unwrap());
626/// assert_eq!(checksummed, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
627/// ```
628fn to_eip55_checksum(address: &[u8; 20]) -> String {
629 // Step 1: Convert to lowercase hex (without 0x prefix)
630 let hex_addr = hex::encode(address);
631
632 // Step 2: Hash the lowercase hex address
633 // Keccak256 always produces exactly 32 bytes
634 let hash: [u8; 32] = Keccak256::digest(hex_addr.as_bytes()).into();
635
636 // Step 3: Build the checksummed address
637 let mut checksummed = String::with_capacity(42);
638 checksummed.push_str("0x");
639
640 for (i, c) in hex_addr.chars().enumerate() {
641 if c.is_ascii_digit() {
642 // Digits don't have case, keep as-is
643 checksummed.push(c);
644 } else {
645 // For letters, check the corresponding nibble in the hash
646 // Each byte in the hash has two nibbles (4 bits each)
647 // Since hex_addr is exactly 40 chars (20 bytes * 2), i/2 ranges from 0 to 19
648 // The hash is 32 bytes, so this indexing is always safe
649 // We use get() for safety even though the bounds are guaranteed
650 let hash_byte = hash.get(i / 2).copied().unwrap_or(0);
651 let nibble = if i % 2 == 0 {
652 (hash_byte >> 4) & 0x0f // High nibble
653 } else {
654 hash_byte & 0x0f // Low nibble
655 };
656
657 // If nibble >= 8, uppercase the letter
658 if nibble >= 8 {
659 checksummed.push(c.to_ascii_uppercase());
660 } else {
661 checksummed.push(c);
662 }
663 }
664 }
665
666 checksummed
667}
668
669// ============================================================================
670// Tests
671// ============================================================================
672
673#[cfg(test)]
674mod tests {
675 #![allow(clippy::expect_used)]
676 #![allow(clippy::indexing_slicing)]
677 #![allow(clippy::panic)]
678 #![allow(clippy::uninlined_format_args)]
679
680 use super::*;
681
682 // ------------------------------------------------------------------------
683 // Chain Enum Tests
684 // ------------------------------------------------------------------------
685
686 #[test]
687 fn test_chain_display() {
688 assert_eq!(Chain::Ethereum.to_string(), "ethereum");
689 assert_eq!(Chain::Bitcoin.to_string(), "bitcoin");
690 assert_eq!(Chain::Solana.to_string(), "solana");
691 assert_eq!(Chain::Tron.to_string(), "tron");
692 assert_eq!(Chain::Ripple.to_string(), "ripple");
693 }
694
695 #[test]
696 fn test_chain_equality() {
697 assert_eq!(Chain::Ethereum, Chain::Ethereum);
698 assert_ne!(Chain::Ethereum, Chain::Bitcoin);
699 }
700
701 #[test]
702 fn test_chain_debug() {
703 let debug_output = format!("{:?}", Chain::Ethereum);
704 assert_eq!(debug_output, "Ethereum");
705 }
706
707 // ------------------------------------------------------------------------
708 // CurveType Enum Tests
709 // ------------------------------------------------------------------------
710
711 #[test]
712 fn test_curve_type_display() {
713 assert_eq!(CurveType::Secp256k1.to_string(), "secp256k1");
714 assert_eq!(CurveType::Ed25519.to_string(), "ed25519");
715 }
716
717 #[test]
718 fn test_curve_type_equality() {
719 assert_eq!(CurveType::Secp256k1, CurveType::Secp256k1);
720 assert_ne!(CurveType::Secp256k1, CurveType::Ed25519);
721 }
722
723 // ------------------------------------------------------------------------
724 // Secp256k1Signer Creation Tests
725 // ------------------------------------------------------------------------
726
727 #[test]
728 fn test_generate_creates_valid_signer() {
729 let signer = Secp256k1Signer::generate();
730
731 // Public key should be 33 bytes (compressed)
732 assert_eq!(signer.public_key().len(), 33);
733
734 // Curve type should be secp256k1
735 assert_eq!(signer.curve(), CurveType::Secp256k1);
736 }
737
738 #[test]
739 fn test_generate_produces_unique_signers() {
740 let signer1 = Secp256k1Signer::generate();
741 let signer2 = Secp256k1Signer::generate();
742
743 // Should generate different public keys
744 assert_ne!(signer1.public_key(), signer2.public_key());
745 }
746
747 #[test]
748 fn test_from_bytes_success() {
749 let bytes = [0x42u8; 32];
750 let result = Secp256k1Signer::from_bytes(bytes);
751 assert!(result.is_ok());
752 }
753
754 #[test]
755 fn test_from_bytes_invalid_zero() {
756 let bytes = [0u8; 32];
757 let result = Secp256k1Signer::from_bytes(bytes);
758 assert!(matches!(result, Err(SignError::InvalidKey)));
759 }
760
761 #[test]
762 fn test_from_bytes_deterministic() {
763 let bytes = [0x42u8; 32];
764
765 let signer1 = Secp256k1Signer::from_bytes(bytes).expect("valid key");
766 let signer2 = Secp256k1Signer::from_bytes(bytes).expect("valid key");
767
768 assert_eq!(signer1.public_key(), signer2.public_key());
769 }
770
771 #[test]
772 fn test_new_from_keypair() {
773 let key_pair = Secp256k1KeyPair::generate();
774 let expected_pubkey = *key_pair.public_key().compressed();
775
776 let signer = Secp256k1Signer::new(key_pair);
777
778 assert_eq!(signer.public_key(), &expected_pubkey);
779 }
780
781 // ------------------------------------------------------------------------
782 // Signing Tests
783 // ------------------------------------------------------------------------
784
785 #[test]
786 fn test_sign_produces_65_bytes() {
787 let signer = Secp256k1Signer::generate();
788 let hash = [0x42u8; 32];
789
790 let signature = signer.sign(&hash).expect("signing should succeed");
791
792 // Signature should be 65 bytes: r (32) || s (32) || v (1)
793 assert_eq!(signature.len(), 65);
794 }
795
796 #[test]
797 fn test_sign_recovery_id_valid() {
798 let signer = Secp256k1Signer::generate();
799 let hash = [0x42u8; 32];
800
801 let signature = signer.sign(&hash).expect("signing should succeed");
802
803 // Recovery ID (last byte) should be 0 or 1
804 let recovery_id = signature[64];
805 assert!(recovery_id == 0 || recovery_id == 1);
806 }
807
808 #[test]
809 fn test_different_hashes_produce_different_signatures() {
810 let signer = Secp256k1Signer::generate();
811 let hash1 = [0x42u8; 32];
812 let hash2 = [0x43u8; 32];
813
814 let sig1 = signer.sign(&hash1).expect("signing should succeed");
815 let sig2 = signer.sign(&hash2).expect("signing should succeed");
816
817 assert_ne!(sig1, sig2);
818 }
819
820 #[test]
821 fn test_sign_is_deterministic_with_same_key() {
822 // Note: ECDSA is NOT deterministic by default (uses random k).
823 // However, the signature components should be consistent with the keypair.
824 let bytes = [0x42u8; 32];
825 let signer = Secp256k1Signer::from_bytes(bytes).expect("valid key");
826 let hash = [0x42u8; 32];
827
828 // Sign twice with same key and hash
829 let sig1 = signer.sign(&hash).expect("signing should succeed");
830 let sig2 = signer.sign(&hash).expect("signing should succeed");
831
832 // Note: These may or may not be equal depending on k256's implementation
833 // k256 uses RFC 6979 deterministic nonces, so they should be equal
834 assert_eq!(sig1, sig2, "k256 uses RFC 6979 deterministic nonces");
835 }
836
837 // ------------------------------------------------------------------------
838 // Ethereum Address Tests
839 // ------------------------------------------------------------------------
840
841 #[test]
842 fn test_ethereum_address_format() {
843 let signer = Secp256k1Signer::generate();
844
845 let address = signer.address(Chain::Ethereum).expect("valid address");
846
847 // Should start with 0x
848 assert!(address.starts_with("0x"));
849
850 // Should be 42 characters (0x + 40 hex chars)
851 assert_eq!(address.len(), 42);
852 }
853
854 #[test]
855 fn test_ethereum_address_deterministic() {
856 let bytes = [0x42u8; 32];
857 let signer = Secp256k1Signer::from_bytes(bytes).expect("valid key");
858
859 let address1 = signer.address(Chain::Ethereum).expect("valid address");
860 let address2 = signer.address(Chain::Ethereum).expect("valid address");
861
862 assert_eq!(address1, address2);
863 }
864
865 /// Test EIP-55 checksum with a known test vector.
866 #[test]
867 fn test_eip55_checksum_known_vector() {
868 // Test vector from EIP-55 specification
869 // Address: 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed
870 let address_hex = "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed";
871 let mut address = [0u8; 20];
872 hex::decode_to_slice(address_hex, &mut address).expect("valid hex");
873
874 let checksummed = to_eip55_checksum(&address);
875
876 assert_eq!(checksummed, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
877 }
878
879 /// Test EIP-55 checksum with all-lowercase address.
880 #[test]
881 fn test_eip55_checksum_all_lowercase() {
882 // fb6916095ca1df60bb79ce92ce3ea74c37c5d359
883 let address_hex = "fb6916095ca1df60bb79ce92ce3ea74c37c5d359";
884 let mut address = [0u8; 20];
885 hex::decode_to_slice(address_hex, &mut address).expect("valid hex");
886
887 let checksummed = to_eip55_checksum(&address);
888
889 assert_eq!(checksummed, "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359");
890 }
891
892 /// Test EIP-55 checksum with another known vector.
893 #[test]
894 fn test_eip55_checksum_vector_2() {
895 // dbf03b407c01e7cd3cbea99509d93f8dddc8c6fb
896 let address_hex = "dbf03b407c01e7cd3cbea99509d93f8dddc8c6fb";
897 let mut address = [0u8; 20];
898 hex::decode_to_slice(address_hex, &mut address).expect("valid hex");
899
900 let checksummed = to_eip55_checksum(&address);
901
902 assert_eq!(checksummed, "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB");
903 }
904
905 /// Test with a real Ethereum address from a known private key.
906 #[test]
907 fn test_ethereum_address_known_private_key() {
908 // This is a well-known test private key
909 // Private key: 0xfad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19
910 let private_key_hex = "fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19";
911 let mut private_key = [0u8; 32];
912 hex::decode_to_slice(private_key_hex, &mut private_key).expect("valid hex");
913
914 let signer = Secp256k1Signer::from_bytes(private_key).expect("valid key");
915 let address = signer.address(Chain::Ethereum).expect("valid address");
916
917 // The expected address (lowercase) is: 0x96216849c49358b10257cb55b28ea603c874b05e
918 // We need to verify the EIP-55 checksummed version
919 let expected_raw = "96216849c49358b10257cb55b28ea603c874b05e";
920 let mut expected_bytes = [0u8; 20];
921 hex::decode_to_slice(expected_raw, &mut expected_bytes).expect("valid hex");
922 let expected_checksummed = to_eip55_checksum(&expected_bytes);
923
924 assert_eq!(address, expected_checksummed);
925 }
926
927 // ------------------------------------------------------------------------
928 // Unsupported Chain Tests
929 // ------------------------------------------------------------------------
930
931 #[test]
932 fn test_bitcoin_address_format() {
933 let signer = Secp256k1Signer::generate();
934
935 let address = signer.address(Chain::Bitcoin).expect("valid address");
936
937 // P2WPKH mainnet addresses start with bc1q
938 assert!(
939 address.starts_with("bc1q"),
940 "Address should start with bc1q: {address}"
941 );
942
943 // P2WPKH addresses are 42 or 62 characters depending on format
944 // bc1q (4) + 38 characters = 42 for standard P2WPKH
945 assert_eq!(
946 address.len(),
947 42,
948 "P2WPKH address should be 42 characters: {address}"
949 );
950 }
951
952 #[test]
953 fn test_bitcoin_address_deterministic() {
954 let bytes = [0x42u8; 32];
955 let signer = Secp256k1Signer::from_bytes(bytes).expect("valid key");
956
957 let address1 = signer.address(Chain::Bitcoin).expect("valid address");
958 let address2 = signer.address(Chain::Bitcoin).expect("valid address");
959
960 assert_eq!(address1, address2);
961 }
962
963 #[test]
964 fn test_bitcoin_address_known_private_key() {
965 // Use a known test private key and verify the address
966 let private_key_hex = "fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19";
967 let mut private_key = [0u8; 32];
968 hex::decode_to_slice(private_key_hex, &mut private_key).expect("valid hex");
969
970 let signer = Secp256k1Signer::from_bytes(private_key).expect("valid key");
971 let address = signer.address(Chain::Bitcoin).expect("valid address");
972
973 // Verify it's a valid bech32 address
974 assert!(address.starts_with("bc1q"));
975 // The address should be deterministic - same key always produces same address
976 let address2 = signer.address(Chain::Bitcoin).expect("valid address");
977 assert_eq!(address, address2);
978 }
979
980 #[test]
981 fn test_tron_address_not_implemented() {
982 let signer = Secp256k1Signer::generate();
983
984 let result = signer.address(Chain::Tron);
985
986 assert!(result.is_err());
987 match result {
988 Err(SignError::SignatureFailed { context }) => {
989 assert!(context.contains("Tron"));
990 }
991 _ => panic!("Expected SignatureFailed error"),
992 }
993 }
994
995 #[test]
996 fn test_ripple_address_not_implemented() {
997 let signer = Secp256k1Signer::generate();
998
999 let result = signer.address(Chain::Ripple);
1000
1001 assert!(result.is_err());
1002 match result {
1003 Err(SignError::SignatureFailed { context }) => {
1004 assert!(context.contains("Ripple"));
1005 }
1006 _ => panic!("Expected SignatureFailed error"),
1007 }
1008 }
1009
1010 #[test]
1011 fn test_solana_address_wrong_curve() {
1012 let signer = Secp256k1Signer::generate();
1013
1014 let result = signer.address(Chain::Solana);
1015
1016 assert!(result.is_err());
1017 match result {
1018 Err(SignError::WrongCurve { expected, actual }) => {
1019 assert_eq!(expected, "ed25519");
1020 assert_eq!(actual, "secp256k1");
1021 }
1022 _ => panic!("Expected WrongCurve error"),
1023 }
1024 }
1025
1026 // ------------------------------------------------------------------------
1027 // Thread Safety Tests
1028 // ------------------------------------------------------------------------
1029
1030 #[test]
1031 fn test_signer_is_send_sync() {
1032 fn assert_send_sync<T: Send + Sync>() {}
1033 assert_send_sync::<Secp256k1Signer>();
1034 }
1035
1036 #[test]
1037 fn test_signer_trait_object_is_send_sync() {
1038 fn assert_send_sync<T: Send + Sync + ?Sized>() {}
1039 assert_send_sync::<dyn Signer>();
1040 }
1041
1042 // ------------------------------------------------------------------------
1043 // Debug Output Tests
1044 // ------------------------------------------------------------------------
1045
1046 #[test]
1047 fn test_signer_debug_output() {
1048 let signer = Secp256k1Signer::generate();
1049 let debug_output = format!("{:?}", signer);
1050
1051 // Should contain the struct name
1052 assert!(debug_output.contains("Secp256k1Signer"));
1053 // Should not expose the private key (inherits from KeyPair's Debug)
1054 }
1055
1056 // ------------------------------------------------------------------------
1057 // Key Pair Access Tests
1058 // ------------------------------------------------------------------------
1059
1060 #[test]
1061 fn test_key_pair_accessor() {
1062 let bytes = [0x42u8; 32];
1063 let signer = Secp256k1Signer::from_bytes(bytes).expect("valid key");
1064
1065 let key_pair = signer.key_pair();
1066
1067 // Should be able to access the underlying key pair
1068 assert_eq!(key_pair.public_key().compressed(), signer.public_key());
1069 }
1070
1071 // ------------------------------------------------------------------------
1072 // EIP-55 Checksum Edge Cases
1073 // ------------------------------------------------------------------------
1074
1075 #[test]
1076 fn test_eip55_checksum_all_digits_address() {
1077 // Address with only digits (no letters to case-transform)
1078 let address_hex = "1234567890123456789012345678901234567890";
1079 let mut address = [0u8; 20];
1080 hex::decode_to_slice(address_hex, &mut address).expect("valid hex");
1081
1082 let checksummed = to_eip55_checksum(&address);
1083
1084 // Should still be all lowercase digits with 0x prefix
1085 assert!(checksummed.starts_with("0x"));
1086 assert_eq!(checksummed.len(), 42);
1087 assert!(checksummed[2..].chars().all(|c| c.is_ascii_digit()));
1088 }
1089
1090 #[test]
1091 fn test_eip55_checksum_boundary_nibbles() {
1092 // Test addresses that exercise boundary conditions in nibble extraction
1093 // High nibbles (even indices) and low nibbles (odd indices)
1094 let test_vectors = vec![
1095 // Address designed to test nibble extraction at different positions
1096 ("0000000000000000000000000000000000000000", 42),
1097 ("ffffffffffffffffffffffffffffffffffffffff", 42),
1098 ("abcdefabcdefabcdefabcdefabcdefabcdefabcd", 42),
1099 ];
1100
1101 for (hex_addr, expected_len) in test_vectors {
1102 let mut address = [0u8; 20];
1103 hex::decode_to_slice(hex_addr, &mut address).expect("valid hex");
1104
1105 let checksummed = to_eip55_checksum(&address);
1106 assert_eq!(checksummed.len(), expected_len);
1107 assert!(checksummed.starts_with("0x"));
1108 }
1109 }
1110
1111 #[test]
1112 fn test_eip55_checksum_hash_byte_boundary() {
1113 // Test that we correctly handle hash byte extraction at i/2
1114 // This exercises the hash.get(i/2) logic
1115 for i in 0..20 {
1116 let mut address = [0u8; 20];
1117 address[i] = 0xAB; // Mix of letters to test case conversion
1118
1119 let checksummed = to_eip55_checksum(&address);
1120 assert_eq!(checksummed.len(), 42);
1121 assert!(checksummed.starts_with("0x"));
1122 }
1123 }
1124
1125 #[test]
1126 fn test_eip55_checksum_even_odd_indices() {
1127 // Specifically test even and odd character indices for nibble extraction
1128 let address_hex = "aabbccddeeff00112233445566778899aabbccdd";
1129 let mut address = [0u8; 20];
1130 hex::decode_to_slice(address_hex, &mut address).expect("valid hex");
1131
1132 let checksummed = to_eip55_checksum(&address);
1133
1134 // Verify format is correct
1135 assert_eq!(checksummed.len(), 42);
1136 assert!(checksummed.starts_with("0x"));
1137
1138 // Verify all characters after 0x are valid hex
1139 assert!(checksummed[2..].chars().all(|c| c.is_ascii_hexdigit()));
1140 }
1141
1142 // ------------------------------------------------------------------------
1143 // Signature Verification Roundtrip Tests
1144 // ------------------------------------------------------------------------
1145
1146 #[test]
1147 fn test_sign_and_verify_with_keypair() {
1148 let bytes = [0x42u8; 32];
1149 let signer = Secp256k1Signer::from_bytes(bytes).expect("valid key");
1150 let hash = [0x42u8; 32];
1151
1152 let signature = signer.sign(&hash).expect("signing should succeed");
1153
1154 // Extract the signature without recovery ID for verification
1155 let sig_bytes: [u8; 64] = signature[..64].try_into().expect("64 bytes");
1156 let sig = crate::keypair::Secp256k1Signature::from_bytes_and_recovery_id(
1157 sig_bytes,
1158 signature[64],
1159 );
1160
1161 // Verify using the key pair
1162 assert!(signer.key_pair().verify(&hash, &sig));
1163 }
1164
1165 // ------------------------------------------------------------------------
1166 // Ed25519Signer Creation Tests
1167 // ------------------------------------------------------------------------
1168
1169 #[test]
1170 fn test_ed25519_generate_creates_valid_signer() {
1171 let signer = Ed25519Signer::generate();
1172
1173 // Public key should be 32 bytes
1174 assert_eq!(signer.public_key().len(), 32);
1175
1176 // Curve type should be ed25519
1177 assert_eq!(signer.curve(), CurveType::Ed25519);
1178 }
1179
1180 #[test]
1181 fn test_ed25519_generate_produces_unique_signers() {
1182 let signer1 = Ed25519Signer::generate();
1183 let signer2 = Ed25519Signer::generate();
1184
1185 // Should generate different public keys
1186 assert_ne!(signer1.public_key(), signer2.public_key());
1187 }
1188
1189 #[test]
1190 fn test_ed25519_from_bytes_success() {
1191 let bytes = [0x42u8; 32];
1192 let result = Ed25519Signer::from_bytes(bytes);
1193 assert!(result.is_ok());
1194 }
1195
1196 #[test]
1197 fn test_ed25519_from_bytes_deterministic() {
1198 let bytes = [0x42u8; 32];
1199
1200 let signer1 = Ed25519Signer::from_bytes(bytes).expect("valid key");
1201 let signer2 = Ed25519Signer::from_bytes(bytes).expect("valid key");
1202
1203 assert_eq!(signer1.public_key(), signer2.public_key());
1204 }
1205
1206 #[test]
1207 fn test_ed25519_new_from_keypair() {
1208 let key_pair = crate::keypair::Ed25519KeyPair::generate();
1209 let expected_pubkey = *key_pair.public_key().as_bytes();
1210
1211 let signer = Ed25519Signer::new(key_pair);
1212
1213 assert_eq!(signer.public_key(), &expected_pubkey);
1214 }
1215
1216 // ------------------------------------------------------------------------
1217 // Ed25519 Signing Tests
1218 // ------------------------------------------------------------------------
1219
1220 #[test]
1221 fn test_ed25519_sign_produces_64_bytes() {
1222 let signer = Ed25519Signer::generate();
1223 let hash = [0x42u8; 32];
1224
1225 let signature = signer.sign(&hash).expect("signing should succeed");
1226
1227 // Ed25519 signature should be 64 bytes
1228 assert_eq!(signature.len(), 64);
1229 }
1230
1231 #[test]
1232 fn test_ed25519_different_hashes_produce_different_signatures() {
1233 let signer = Ed25519Signer::generate();
1234 let hash1 = [0x42u8; 32];
1235 let hash2 = [0x43u8; 32];
1236
1237 let sig1 = signer.sign(&hash1).expect("signing should succeed");
1238 let sig2 = signer.sign(&hash2).expect("signing should succeed");
1239
1240 assert_ne!(sig1, sig2);
1241 }
1242
1243 #[test]
1244 fn test_ed25519_sign_is_deterministic() {
1245 // Ed25519 signatures are deterministic (no random k)
1246 let bytes = [0x42u8; 32];
1247 let signer = Ed25519Signer::from_bytes(bytes).expect("valid key");
1248 let hash = [0x42u8; 32];
1249
1250 let sig1 = signer.sign(&hash).expect("signing should succeed");
1251 let sig2 = signer.sign(&hash).expect("signing should succeed");
1252
1253 assert_eq!(sig1, sig2, "ed25519 signatures should be deterministic");
1254 }
1255
1256 // ------------------------------------------------------------------------
1257 // Ed25519 Solana Address Tests
1258 // ------------------------------------------------------------------------
1259
1260 #[test]
1261 fn test_ed25519_solana_address_format() {
1262 let signer = Ed25519Signer::generate();
1263
1264 let address = signer.address(Chain::Solana).expect("valid address");
1265
1266 // Solana addresses are base58 encoded, typically 32-44 characters
1267 assert!(
1268 address.len() >= 32 && address.len() <= 44,
1269 "Solana address should be 32-44 characters: {address}"
1270 );
1271 }
1272
1273 #[test]
1274 fn test_ed25519_solana_address_deterministic() {
1275 let bytes = [0x42u8; 32];
1276 let signer = Ed25519Signer::from_bytes(bytes).expect("valid key");
1277
1278 let address1 = signer.address(Chain::Solana).expect("valid address");
1279 let address2 = signer.address(Chain::Solana).expect("valid address");
1280
1281 assert_eq!(address1, address2);
1282 }
1283
1284 // ------------------------------------------------------------------------
1285 // Ed25519 Wrong Curve Tests
1286 // ------------------------------------------------------------------------
1287
1288 #[test]
1289 fn test_ed25519_ethereum_address_wrong_curve() {
1290 let signer = Ed25519Signer::generate();
1291
1292 let result = signer.address(Chain::Ethereum);
1293
1294 assert!(result.is_err());
1295 match result {
1296 Err(SignError::WrongCurve { expected, actual }) => {
1297 assert_eq!(expected, "secp256k1");
1298 assert_eq!(actual, "ed25519");
1299 }
1300 _ => panic!("Expected WrongCurve error"),
1301 }
1302 }
1303
1304 #[test]
1305 fn test_ed25519_bitcoin_address_wrong_curve() {
1306 let signer = Ed25519Signer::generate();
1307
1308 let result = signer.address(Chain::Bitcoin);
1309
1310 assert!(result.is_err());
1311 match result {
1312 Err(SignError::WrongCurve { expected, actual }) => {
1313 assert_eq!(expected, "secp256k1");
1314 assert_eq!(actual, "ed25519");
1315 }
1316 _ => panic!("Expected WrongCurve error"),
1317 }
1318 }
1319
1320 #[test]
1321 fn test_ed25519_tron_address_wrong_curve() {
1322 let signer = Ed25519Signer::generate();
1323
1324 let result = signer.address(Chain::Tron);
1325
1326 assert!(result.is_err());
1327 match result {
1328 Err(SignError::WrongCurve { expected, actual }) => {
1329 assert_eq!(expected, "secp256k1");
1330 assert_eq!(actual, "ed25519");
1331 }
1332 _ => panic!("Expected WrongCurve error"),
1333 }
1334 }
1335
1336 #[test]
1337 fn test_ed25519_ripple_address_wrong_curve() {
1338 let signer = Ed25519Signer::generate();
1339
1340 let result = signer.address(Chain::Ripple);
1341
1342 assert!(result.is_err());
1343 match result {
1344 Err(SignError::WrongCurve { expected, actual }) => {
1345 assert_eq!(expected, "secp256k1");
1346 assert_eq!(actual, "ed25519");
1347 }
1348 _ => panic!("Expected WrongCurve error"),
1349 }
1350 }
1351
1352 // ------------------------------------------------------------------------
1353 // Ed25519 Thread Safety Tests
1354 // ------------------------------------------------------------------------
1355
1356 #[test]
1357 fn test_ed25519_signer_is_send_sync() {
1358 fn assert_send_sync<T: Send + Sync>() {}
1359 assert_send_sync::<Ed25519Signer>();
1360 }
1361
1362 // ------------------------------------------------------------------------
1363 // Ed25519 Debug and Key Pair Access Tests
1364 // ------------------------------------------------------------------------
1365
1366 #[test]
1367 fn test_ed25519_signer_debug_output() {
1368 let signer = Ed25519Signer::generate();
1369 let debug_output = format!("{:?}", signer);
1370
1371 // Should contain the struct name
1372 assert!(debug_output.contains("Ed25519Signer"));
1373 }
1374
1375 #[test]
1376 fn test_ed25519_key_pair_accessor() {
1377 let bytes = [0x42u8; 32];
1378 let signer = Ed25519Signer::from_bytes(bytes).expect("valid key");
1379
1380 let key_pair = signer.key_pair();
1381
1382 // Should be able to access the underlying key pair
1383 assert_eq!(key_pair.public_key().as_bytes(), signer.public_key());
1384 }
1385
1386 // ------------------------------------------------------------------------
1387 // Ed25519 Sign and Verify Roundtrip
1388 // ------------------------------------------------------------------------
1389
1390 #[test]
1391 fn test_ed25519_sign_and_verify_with_keypair() {
1392 let bytes = [0x42u8; 32];
1393 let signer = Ed25519Signer::from_bytes(bytes).expect("valid key");
1394 let hash = [0x42u8; 32];
1395
1396 let signature = signer.sign(&hash).expect("signing should succeed");
1397
1398 // Convert signature bytes to Ed25519Signature
1399 let sig_bytes: [u8; 64] = signature.try_into().expect("64 bytes");
1400 let sig = crate::keypair::Ed25519Signature::from_bytes(sig_bytes);
1401
1402 // Verify using the key pair
1403 assert!(signer.key_pair().verify(&hash, &sig));
1404 }
1405}
1406
1407#[cfg(test)]
1408mod proptest_tests {
1409 #![allow(clippy::expect_used)]
1410 #![allow(clippy::indexing_slicing)]
1411
1412 use super::*;
1413 use proptest::prelude::*;
1414
1415 proptest! {
1416 #[test]
1417 fn test_sign_always_produces_65_bytes(hash in any::<[u8; 32]>()) {
1418 let signer = Secp256k1Signer::generate();
1419 let signature = signer.sign(&hash).expect("signing should succeed");
1420 prop_assert_eq!(signature.len(), 65);
1421 }
1422
1423 #[test]
1424 fn test_recovery_id_always_valid(hash in any::<[u8; 32]>()) {
1425 let signer = Secp256k1Signer::generate();
1426 let signature = signer.sign(&hash).expect("signing should succeed");
1427 let recovery_id = signature[64];
1428 prop_assert!(recovery_id <= 1);
1429 }
1430
1431 #[test]
1432 fn test_ethereum_address_always_valid_format(seed in any::<[u8; 32]>()) {
1433 // Skip invalid seeds
1434 if seed == [0u8; 32] {
1435 return Ok(());
1436 }
1437
1438 if let Ok(signer) = Secp256k1Signer::from_bytes(seed) {
1439 let address = signer.address(Chain::Ethereum).expect("valid address");
1440
1441 // Should start with 0x
1442 prop_assert!(address.starts_with("0x"));
1443
1444 // Should be 42 characters
1445 prop_assert_eq!(address.len(), 42);
1446
1447 // Remaining characters should be valid hex
1448 prop_assert!(address[2..].chars().all(|c| c.is_ascii_hexdigit()));
1449 }
1450 }
1451 }
1452}