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