Skip to main content

chains_sdk/bitcoin/
mod.rs

1//! Bitcoin ECDSA signer using secp256k1 + double-SHA-256.
2//!
3//! Implements RFC 6979 deterministic nonces (built into k256),
4//! strict DER-encoded signature output, and double-SHA-256 hashing.
5
6pub mod descriptor;
7pub mod helpers;
8pub mod message;
9pub mod psbt;
10pub mod schnorr;
11pub mod scripts;
12pub mod sighash;
13pub mod taproot;
14pub mod tapscript;
15pub mod transaction;
16
17use crate::crypto;
18use crate::encoding;
19use crate::error::SignerError;
20use crate::traits;
21use k256::ecdsa::signature::hazmat::PrehashSigner;
22use k256::ecdsa::signature::hazmat::PrehashVerifier;
23use k256::ecdsa::{Signature as K256Signature, SigningKey, VerifyingKey};
24use zeroize::Zeroizing;
25
26/// A Bitcoin ECDSA signature in DER encoding.
27#[derive(Debug, Clone, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29#[must_use]
30pub struct BitcoinSignature {
31    /// DER-encoded signature bytes (private to prevent mutation).
32    der_bytes: Vec<u8>,
33}
34
35impl BitcoinSignature {
36    /// Export the DER-encoded signature bytes.
37    #[must_use]
38    pub fn to_bytes(&self) -> Vec<u8> {
39        self.der_bytes.clone()
40    }
41
42    /// Get a reference to the DER-encoded signature bytes.
43    #[must_use]
44    pub fn der_bytes(&self) -> &[u8] {
45        &self.der_bytes
46    }
47
48    /// Import from DER-encoded signature bytes.
49    pub fn from_bytes(der: &[u8]) -> Result<Self, SignerError> {
50        // Validate it's a valid DER ECDSA signature
51        K256Signature::from_der(der).map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
52        Ok(Self {
53            der_bytes: der.to_vec(),
54        })
55    }
56}
57
58impl core::fmt::Display for BitcoinSignature {
59    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60        for byte in &self.der_bytes {
61            write!(f, "{byte:02x}")?;
62        }
63        Ok(())
64    }
65}
66
67/// Double-SHA-256: SHA256(SHA256(data)).
68pub fn double_sha256(data: &[u8]) -> [u8; 32] {
69    crypto::double_sha256(data)
70}
71
72/// Bitcoin ECDSA signer.
73///
74/// Uses secp256k1 with double-SHA-256 hashing and RFC 6979 deterministic nonces.
75/// Produces strict DER-encoded signatures.
76pub struct BitcoinSigner {
77    signing_key: SigningKey,
78}
79
80// k256::SigningKey implements ZeroizeOnDrop internally — no explicit Drop needed.
81
82impl BitcoinSigner {
83    /// Sign a pre-computed 32-byte digest.
84    fn sign_digest(&self, digest: &[u8; 32]) -> Result<BitcoinSignature, SignerError> {
85        let sig: K256Signature = self
86            .signing_key
87            .sign_prehash(digest)
88            .map_err(|e| SignerError::SigningFailed(e.to_string()))?;
89
90        // Encode as strict DER
91        let der = sig.to_der();
92        Ok(BitcoinSignature {
93            der_bytes: der.as_bytes().to_vec(),
94        })
95    }
96
97    /// Export the private key in **WIF** (Wallet Import Format).
98    ///
99    /// Uses version byte 0x80 (mainnet) with compression flag.
100    /// Result starts with `K` or `L`.
101    ///
102    /// # Security
103    /// The returned String contains the private key — handle with care.
104    #[must_use]
105    pub fn to_wif(&self) -> Zeroizing<String> {
106        let mut payload = Zeroizing::new(Vec::with_capacity(38));
107        payload.push(0x80); // mainnet version
108        payload.extend_from_slice(&self.signing_key.to_bytes());
109        payload.push(0x01); // compressed flag
110        let checksum = double_sha256(&payload);
111        payload.extend_from_slice(&checksum[..4]);
112        Zeroizing::new(bs58::encode(&*payload).into_string())
113    }
114
115    /// Export the private key in **testnet WIF** format.
116    ///
117    /// Uses version byte 0xEF (testnet). Result starts with `c`.
118    ///
119    /// # Security
120    /// The returned String contains the private key — handle with care.
121    #[must_use]
122    pub fn to_wif_testnet(&self) -> Zeroizing<String> {
123        let mut payload = Zeroizing::new(Vec::with_capacity(38));
124        payload.push(0xEF); // testnet version
125        payload.extend_from_slice(&self.signing_key.to_bytes());
126        payload.push(0x01); // compressed flag
127        let checksum = double_sha256(&payload);
128        payload.extend_from_slice(&checksum[..4]);
129        Zeroizing::new(bs58::encode(&*payload).into_string())
130    }
131
132    /// Import a private key from **WIF** (Wallet Import Format).
133    ///
134    /// Accepts mainnet (`5`/`K`/`L`) and testnet (`9`/`c`) WIF strings.
135    pub fn from_wif(wif: &str) -> Result<Self, SignerError> {
136        use crate::traits::KeyPair;
137        let decoded = Zeroizing::new(
138            bs58::decode(wif)
139                .into_vec()
140                .map_err(|e| SignerError::InvalidPrivateKey(format!("invalid WIF base58: {e}")))?,
141        );
142
143        // Validate length: 37 (uncompressed) or 38 (compressed)
144        if decoded.len() != 37 && decoded.len() != 38 {
145            return Err(SignerError::InvalidPrivateKey(format!(
146                "WIF must be 37 or 38 bytes, got {}",
147                decoded.len()
148            )));
149        }
150
151        // Validate version byte
152        let version = decoded[0];
153        if version != 0x80 && version != 0xEF {
154            return Err(SignerError::InvalidPrivateKey(format!(
155                "invalid WIF version: 0x{version:02x}"
156            )));
157        }
158
159        // Validate checksum (constant-time comparison)
160        let payload_len = decoded.len() - 4;
161        let checksum = double_sha256(&decoded[..payload_len]);
162        use subtle::ConstantTimeEq;
163        if decoded[payload_len..].ct_eq(&checksum[..4]).unwrap_u8() != 1 {
164            return Err(SignerError::InvalidPrivateKey(
165                "invalid WIF checksum".into(),
166            ));
167        }
168
169        // Extract key bytes (skip version byte; compression flag handled by length check)
170        let key_bytes = &decoded[1..33];
171
172        Self::from_bytes(key_bytes)
173    }
174
175    /// Generate a **P2PKH** address (`1...`) from the compressed public key.
176    ///
177    /// Formula: Base58Check(0x00 || HASH160(compressed_pubkey))
178    #[must_use]
179    pub fn p2pkh_address(&self) -> String {
180        let pubkey = self.signing_key.verifying_key().to_sec1_bytes();
181        let h160 = hash160(&pubkey);
182        base58check_encode(0x00, &h160)
183    }
184
185    /// Generate a **P2WPKH** (SegWit) address (`bc1...`) from the compressed public key.
186    ///
187    /// Formula: Bech32("bc", 0, HASH160(compressed_pubkey))
188    pub fn p2wpkh_address(&self) -> Result<String, SignerError> {
189        let pubkey = self.signing_key.verifying_key().to_sec1_bytes();
190        let h160 = hash160(&pubkey);
191        bech32_encode("bc", 0, &h160)
192    }
193
194    /// Generate a **testnet P2PKH** address (`m...` or `n...`).
195    pub fn p2pkh_testnet_address(&self) -> String {
196        let pubkey = self.signing_key.verifying_key().to_sec1_bytes();
197        let h160 = hash160(&pubkey);
198        base58check_encode(0x6F, &h160) // testnet version byte
199    }
200
201    /// Generate a **testnet P2WPKH** address (`tb1q...`).
202    pub fn p2wpkh_testnet_address(&self) -> Result<String, SignerError> {
203        let pubkey = self.signing_key.verifying_key().to_sec1_bytes();
204        let h160 = hash160(&pubkey);
205        bech32_encode("tb", 0, &h160)
206    }
207
208    /// **BIP-137**: Sign a message with the Bitcoin Signed Message prefix.
209    ///
210    /// Computes `double_sha256("\x18Bitcoin Signed Message:\n" || varint(len) || message)`
211    /// and signs the resulting 32-byte digest.
212    pub fn sign_message(&self, message: &[u8]) -> Result<BitcoinSignature, SignerError> {
213        let digest = bitcoin_message_hash(message);
214        self.sign_digest(&digest)
215    }
216}
217
218/// HASH160: RIPEMD160(SHA256(data)) — the standard Bitcoin hash function.
219pub fn hash160(data: &[u8]) -> [u8; 20] {
220    crypto::hash160(data)
221}
222
223/// Base58Check encode: `version_byte || payload || checksum[0..4]`.
224fn base58check_encode(version: u8, payload: &[u8]) -> String {
225    encoding::base58check_encode(version, payload)
226}
227
228/// Bech32/Bech32m encode for SegWit/Taproot addresses.
229pub(crate) fn bech32_encode(
230    hrp: &str,
231    witness_version: u8,
232    program: &[u8],
233) -> Result<String, SignerError> {
234    encoding::bech32_encode(hrp, witness_version, program)
235}
236
237/// **BIP-137**: Hash a message with the Bitcoin Signed Message prefix.
238///
239/// `double_sha256("\x18Bitcoin Signed Message:\n" || varint(len) || message)`
240pub fn bitcoin_message_hash(message: &[u8]) -> [u8; 32] {
241    let mut data = Vec::new();
242    // Prefix: "\x18Bitcoin Signed Message:\n"
243    data.extend_from_slice(b"\x18Bitcoin Signed Message:\n");
244    // Varint-encoded message length
245    data.extend_from_slice(&varint_encode(message.len()));
246    data.extend_from_slice(message);
247    double_sha256(&data)
248}
249
250/// Bitcoin variable-length integer encoding.
251fn varint_encode(n: usize) -> Vec<u8> {
252    let mut buf = Vec::new();
253    encoding::encode_compact_size(&mut buf, n as u64);
254    buf
255}
256
257/// Validate a Bitcoin address string.
258///
259/// Returns `true` if the address is a valid P2PKH (`1...`), P2SH (`3...`),
260/// P2WPKH (`bc1q...`), or P2TR (`bc1p...`) address.
261pub fn validate_address(address: &str) -> bool {
262    validate_mainnet_address(address) || validate_testnet_address(address)
263}
264
265/// Validate a mainnet Bitcoin address.
266pub fn validate_mainnet_address(address: &str) -> bool {
267    if address.starts_with("bc1") {
268        // Bech32/Bech32m
269        bech32::segwit::decode(address).is_ok()
270    } else if address.starts_with('1') || address.starts_with('3') {
271        // Base58Check (P2PKH or P2SH)
272        validate_base58check(address, &[0x00, 0x05])
273    } else {
274        false
275    }
276}
277
278/// Validate a testnet Bitcoin address.
279pub fn validate_testnet_address(address: &str) -> bool {
280    if address.starts_with("tb1") {
281        bech32::segwit::decode(address).is_ok()
282    } else if address.starts_with('m') || address.starts_with('n') || address.starts_with('2') {
283        validate_base58check(address, &[0x6F, 0xC4])
284    } else {
285        false
286    }
287}
288
289/// Validate a Base58Check-encoded address has a valid checksum and version byte.
290fn validate_base58check(address: &str, valid_versions: &[u8]) -> bool {
291    let decoded = match bs58::decode(address).into_vec() {
292        Ok(d) => d,
293        Err(_) => return false,
294    };
295    if decoded.len() != 25 {
296        return false;
297    }
298    // Verify version byte
299    if !valid_versions.contains(&decoded[0]) {
300        return false;
301    }
302    // Verify checksum
303    let checksum = double_sha256(&decoded[..21]);
304    decoded[21..25] == checksum[..4]
305}
306
307impl traits::Signer for BitcoinSigner {
308    type Signature = BitcoinSignature;
309    type Error = SignerError;
310
311    fn sign(&self, message: &[u8]) -> Result<BitcoinSignature, SignerError> {
312        let digest = double_sha256(message);
313        self.sign_digest(&digest)
314    }
315
316    fn sign_prehashed(&self, digest: &[u8]) -> Result<BitcoinSignature, SignerError> {
317        if digest.len() != 32 {
318            return Err(SignerError::InvalidHashLength {
319                expected: 32,
320                got: digest.len(),
321            });
322        }
323        let mut hash = [0u8; 32];
324        hash.copy_from_slice(digest);
325        self.sign_digest(&hash)
326    }
327
328    fn public_key_bytes(&self) -> Vec<u8> {
329        self.signing_key.verifying_key().to_sec1_bytes().to_vec()
330    }
331
332    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
333        self.signing_key
334            .verifying_key()
335            .to_encoded_point(false)
336            .as_bytes()
337            .to_vec()
338    }
339}
340
341impl traits::KeyPair for BitcoinSigner {
342    fn generate() -> Result<Self, SignerError> {
343        let mut key_bytes = zeroize::Zeroizing::new([0u8; 32]);
344        crate::security::secure_random(&mut *key_bytes)?;
345        let signing_key = SigningKey::from_bytes((&*key_bytes).into())
346            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
347        Ok(Self { signing_key })
348    }
349
350    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
351        if private_key.len() != 32 {
352            return Err(SignerError::InvalidPrivateKey(format!(
353                "expected 32 bytes, got {}",
354                private_key.len()
355            )));
356        }
357        let signing_key = SigningKey::from_bytes(private_key.into())
358            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
359        Ok(Self { signing_key })
360    }
361
362    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
363        Zeroizing::new(self.signing_key.to_bytes().to_vec())
364    }
365}
366
367/// Bitcoin ECDSA verifier.
368///
369/// # Verification Semantics
370/// - `Ok(true)` — signature is valid.
371/// - `Ok(false)` — signature is mathematically invalid (wrong key or tampered data).
372/// - `Err(...)` — signature could not be parsed (malformed DER encoding, wrong length, etc.).
373pub struct BitcoinVerifier {
374    verifying_key: VerifyingKey,
375}
376
377impl BitcoinVerifier {
378    /// Create from compressed or uncompressed public key bytes.
379    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
380        let verifying_key = VerifyingKey::from_sec1_bytes(bytes)
381            .map_err(|e| SignerError::InvalidPublicKey(e.to_string()))?;
382        Ok(Self { verifying_key })
383    }
384
385    fn verify_digest(
386        &self,
387        digest: &[u8; 32],
388        signature: &BitcoinSignature,
389    ) -> Result<bool, SignerError> {
390        let sig = K256Signature::from_der(&signature.der_bytes)
391            .map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
392        match self.verifying_key.verify_prehash(digest, &sig) {
393            Ok(()) => Ok(true),
394            Err(_) => Ok(false),
395        }
396    }
397}
398
399impl traits::Verifier for BitcoinVerifier {
400    type Signature = BitcoinSignature;
401    type Error = SignerError;
402
403    fn verify(&self, message: &[u8], signature: &BitcoinSignature) -> Result<bool, SignerError> {
404        let digest = double_sha256(message);
405        self.verify_digest(&digest, signature)
406    }
407
408    fn verify_prehashed(
409        &self,
410        digest: &[u8],
411        signature: &BitcoinSignature,
412    ) -> Result<bool, SignerError> {
413        if digest.len() != 32 {
414            return Err(SignerError::InvalidHashLength {
415                expected: 32,
416                got: digest.len(),
417            });
418        }
419        let mut hash = [0u8; 32];
420        hash.copy_from_slice(digest);
421        self.verify_digest(&hash, signature)
422    }
423}
424
425#[cfg(test)]
426#[allow(clippy::unwrap_used, clippy::expect_used)]
427mod tests {
428    use super::*;
429    use crate::traits::{KeyPair, Signer, Verifier};
430
431    #[test]
432    fn test_generate_keypair() {
433        let signer = BitcoinSigner::generate().unwrap();
434        let pubkey = signer.public_key_bytes();
435        assert_eq!(pubkey.len(), 33); // compressed
436    }
437
438    #[test]
439    fn test_from_bytes_roundtrip() {
440        let signer = BitcoinSigner::generate().unwrap();
441        let key_bytes = signer.private_key_bytes();
442        let restored = BitcoinSigner::from_bytes(&key_bytes).unwrap();
443        assert_eq!(signer.public_key_bytes(), restored.public_key_bytes());
444    }
445
446    #[test]
447    fn test_sign_verify_roundtrip() {
448        let signer = BitcoinSigner::generate().unwrap();
449        let msg = b"hello bitcoin";
450        let sig = signer.sign(msg).unwrap();
451        let verifier = BitcoinVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
452        assert!(verifier.verify(msg, &sig).unwrap());
453    }
454
455    #[test]
456    fn test_double_sha256() {
457        // Known test: SHA256(SHA256("hello")) =
458        // 9595c9df90075148eb06860365df33584b75bff782a510c6cd4883a419833d50
459        let result = double_sha256(b"hello");
460        assert_eq!(
461            hex::encode(result),
462            "9595c9df90075148eb06860365df33584b75bff782a510c6cd4883a419833d50"
463        );
464    }
465
466    #[test]
467    fn test_rfc6979_deterministic() {
468        // Same (key, msg) must produce identical signature every time (RFC 6979)
469        let privkey =
470            hex::decode("0000000000000000000000000000000000000000000000000000000000000001")
471                .unwrap();
472        let signer = BitcoinSigner::from_bytes(&privkey).unwrap();
473        let sig1 = signer.sign(b"Satoshi Nakamoto").unwrap();
474        let sig2 = signer.sign(b"Satoshi Nakamoto").unwrap();
475        assert_eq!(sig1.der_bytes(), sig2.der_bytes());
476    }
477
478    #[test]
479    fn test_rfc6979_known_vector_privkey_1() {
480        // Private key = 1, message = "Satoshi Nakamoto"
481        // This is a well-known Bitcoin Core test vector for RFC 6979 deterministic nonce.
482        let privkey =
483            hex::decode("0000000000000000000000000000000000000000000000000000000000000001")
484                .unwrap();
485        let signer = BitcoinSigner::from_bytes(&privkey).unwrap();
486
487        // Verify the public key for private key = 1
488        let pubkey = signer.public_key_bytes();
489        assert_eq!(
490            hex::encode(&pubkey).to_uppercase(),
491            "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"
492        );
493
494        // Sign "Satoshi Nakamoto" with double-SHA256
495        let sig = signer.sign(b"Satoshi Nakamoto").unwrap();
496        // DER signature must be valid and deterministic
497        assert!(!sig.der_bytes().is_empty());
498        // Verify it
499        let verifier = BitcoinVerifier::from_public_key_bytes(&pubkey).unwrap();
500        assert!(verifier.verify(b"Satoshi Nakamoto", &sig).unwrap());
501    }
502
503    #[test]
504    fn test_rfc6979_known_vector_privkey_2() {
505        // Another well-known vector: private key = 2
506        let privkey =
507            hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
508                .unwrap();
509        let signer = BitcoinSigner::from_bytes(&privkey).unwrap();
510        let sig = signer.sign(b"Satoshi Nakamoto").unwrap();
511        let verifier = BitcoinVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
512        assert!(verifier.verify(b"Satoshi Nakamoto", &sig).unwrap());
513        // Deterministic: sign again and compare
514        let sig2 = signer.sign(b"Satoshi Nakamoto").unwrap();
515        assert_eq!(sig.der_bytes(), sig2.der_bytes());
516    }
517
518    #[test]
519    fn test_der_encoding() {
520        let signer = BitcoinSigner::generate().unwrap();
521        let sig = signer.sign(b"DER test").unwrap();
522        // DER signatures start with 0x30 (SEQUENCE tag)
523        assert_eq!(sig.der_bytes()[0], 0x30);
524        // Length should be reasonable (70-72 bytes typically)
525        assert!(sig.der_bytes().len() >= 68 && sig.der_bytes().len() <= 72);
526    }
527
528    #[test]
529    fn test_invalid_privkey_rejected() {
530        assert!(BitcoinSigner::from_bytes(&[0u8; 32]).is_err());
531        assert!(BitcoinSigner::from_bytes(&[1u8; 31]).is_err());
532        assert!(BitcoinSigner::from_bytes(&[1u8; 33]).is_err());
533    }
534
535    #[test]
536    fn test_tampered_sig_fails() {
537        let signer = BitcoinSigner::generate().unwrap();
538        let sig = signer.sign(b"tamper test").unwrap();
539        let verifier = BitcoinVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
540
541        let tampered_bytes: Vec<u8> = {
542            let mut b = sig.to_bytes();
543            if let Some(byte) = b.last_mut() {
544                *byte ^= 0xff;
545            }
546            b
547        };
548        let tampered = BitcoinSignature::from_bytes(&tampered_bytes);
549        // Tampered DER may fail to parse (Err) or verify as invalid (Ok(false))
550        if let Ok(t) = tampered {
551            let result = verifier.verify(b"tamper test", &t);
552            assert!(result.is_err() || !result.unwrap());
553        }
554    }
555
556    #[test]
557    fn test_wrong_pubkey_fails() {
558        let signer1 = BitcoinSigner::generate().unwrap();
559        let signer2 = BitcoinSigner::generate().unwrap();
560        let sig = signer1.sign(b"wrong key test").unwrap();
561        let verifier = BitcoinVerifier::from_public_key_bytes(&signer2.public_key_bytes()).unwrap();
562        assert!(!verifier.verify(b"wrong key test", &sig).unwrap());
563    }
564
565    #[test]
566    fn test_empty_message() {
567        let signer = BitcoinSigner::generate().unwrap();
568        let sig = signer.sign(b"").unwrap();
569        let verifier = BitcoinVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
570        assert!(verifier.verify(b"", &sig).unwrap());
571    }
572
573    #[test]
574    fn test_sign_prehashed_roundtrip() {
575        let signer = BitcoinSigner::generate().unwrap();
576        let msg = b"prehash btc";
577        let digest = double_sha256(msg);
578        let sig = signer.sign_prehashed(&digest).unwrap();
579        let verifier = BitcoinVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
580        assert!(verifier.verify_prehashed(&digest, &sig).unwrap());
581    }
582
583    #[test]
584    fn test_zeroize_on_drop() {
585        let signer = BitcoinSigner::generate().unwrap();
586        let key_bytes = signer.private_key_bytes();
587        let _: Zeroizing<Vec<u8>> = key_bytes;
588        drop(signer);
589    }
590
591    // ─── Known Address Vectors ──────────────────────────────────
592
593    #[test]
594    fn test_p2pkh_known_address_privkey_1() {
595        // Private key = 1 → generator point G
596        // Compressed P2PKH = 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
597        let sk = hex::decode("0000000000000000000000000000000000000000000000000000000000000001")
598            .unwrap();
599        let signer = BitcoinSigner::from_bytes(&sk).unwrap();
600        assert_eq!(signer.p2pkh_address(), "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH");
601    }
602
603    #[test]
604    fn test_p2wpkh_known_address_privkey_1() {
605        // Private key = 1 → bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
606        let sk = hex::decode("0000000000000000000000000000000000000000000000000000000000000001")
607            .unwrap();
608        let signer = BitcoinSigner::from_bytes(&sk).unwrap();
609        assert_eq!(
610            signer.p2wpkh_address().unwrap(),
611            "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
612        );
613    }
614
615    // ─── WIF Round-Trip ─────────────────────────────────────────
616
617    #[test]
618    fn test_wif_encode_known() {
619        // Private key = 1 → known WIF
620        let sk = hex::decode("0000000000000000000000000000000000000000000000000000000000000001")
621            .unwrap();
622        let signer = BitcoinSigner::from_bytes(&sk).unwrap();
623        let wif = signer.to_wif();
624        assert!(wif.starts_with('K') || wif.starts_with('L'));
625        assert_eq!(wif.len(), 52); // compressed WIF is 52 chars
626    }
627
628    #[test]
629    fn test_wif_roundtrip() {
630        let signer = BitcoinSigner::generate().unwrap();
631        let wif = signer.to_wif();
632        let restored = BitcoinSigner::from_wif(&wif).unwrap();
633        assert_eq!(&*signer.private_key_bytes(), &*restored.private_key_bytes());
634        assert_eq!(signer.p2pkh_address(), restored.p2pkh_address());
635    }
636
637    // ─── Address Validation ─────────────────────────────────────
638
639    #[test]
640    fn test_validate_known_addresses() {
641        assert!(validate_address("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH")); // P2PKH
642        assert!(validate_address(
643            "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
644        )); // P2WPKH
645        assert!(!validate_address(""));
646        assert!(!validate_address("1invalid"));
647        assert!(!validate_address("bc1qinvalid"));
648    }
649
650    // ─── BIP-137 Message Signing ────────────────────────────────
651
652    #[test]
653    fn test_bip137_sign_verify_roundtrip() {
654        let signer = BitcoinSigner::generate().unwrap();
655        let sig = signer.sign_message(b"Hello Bitcoin").unwrap();
656        // BIP-137 uses the same ECDSA path → DER encoded
657        assert_eq!(sig.der_bytes()[0], 0x30); // DER SEQUENCE tag
658                                              // Verify round-trip
659        let verifier = BitcoinVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
660        let digest = bitcoin_message_hash(b"Hello Bitcoin");
661        assert!(verifier.verify_prehashed(&digest, &sig).unwrap());
662    }
663}