Skip to main content

chains_sdk/bitcoin/
descriptor.rs

1//! **BIP-380-386** — Output Script Descriptors.
2//!
3//! Human-readable descriptors for Bitcoin output scripts, supporting
4//! legacy (pkh), SegWit (wpkh, wsh), and Taproot (tr) formats.
5//!
6//! # Example
7//! ```no_run
8//! use chains_sdk::bitcoin::descriptor::{Descriptor, DescriptorKey};
9//!
10//! fn main() -> Result<(), Box<dyn std::error::Error>> {
11//!     let key = DescriptorKey::from_hex("0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798")?;
12//!     let desc = Descriptor::wpkh(key);
13//!     println!("Address: {}", desc.address("bc")?);
14//!     Ok(())
15//! }
16//! ```
17
18use crate::crypto;
19use crate::encoding;
20use crate::error::SignerError;
21
22// ─── Descriptor Key ─────────────────────────────────────────────────
23
24/// A public key for use in descriptors.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum DescriptorKey {
27    /// A compressed public key (33 bytes).
28    Compressed([u8; 33]),
29    /// An x-only public key (32 bytes, for Taproot).
30    XOnly([u8; 32]),
31}
32
33impl DescriptorKey {
34    /// Parse a hex-encoded public key.
35    pub fn from_hex(hex_str: &str) -> Result<Self, SignerError> {
36        let bytes =
37            hex::decode(hex_str).map_err(|e| SignerError::ParseError(format!("hex: {e}")))?;
38        match bytes.len() {
39            33 => {
40                let mut key = [0u8; 33];
41                key.copy_from_slice(&bytes);
42                Ok(DescriptorKey::Compressed(key))
43            }
44            32 => {
45                let mut key = [0u8; 32];
46                key.copy_from_slice(&bytes);
47                Ok(DescriptorKey::XOnly(key))
48            }
49            _ => Err(SignerError::ParseError(format!(
50                "invalid key length: {}",
51                bytes.len()
52            ))),
53        }
54    }
55
56    /// Get the compressed public key bytes.
57    pub fn compressed_bytes(&self) -> Option<&[u8; 33]> {
58        match self {
59            DescriptorKey::Compressed(k) => Some(k),
60            DescriptorKey::XOnly(_) => None,
61        }
62    }
63
64    /// Get the x-only key bytes.
65    pub fn x_only_bytes(&self) -> Option<&[u8; 32]> {
66        match self {
67            DescriptorKey::XOnly(k) => Some(k),
68            DescriptorKey::Compressed(_) => None,
69        }
70    }
71
72    /// Calculate HASH160 of the compressed key (for P2PKH / P2WPKH).
73    pub fn hash160(&self) -> Option<[u8; 20]> {
74        match self {
75            DescriptorKey::Compressed(key) => Some(crypto::hash160(key)),
76            DescriptorKey::XOnly(_) => None,
77        }
78    }
79}
80
81// ─── Descriptor Types ───────────────────────────────────────────────
82
83/// A Bitcoin output script descriptor.
84#[derive(Clone, Debug)]
85pub enum Descriptor {
86    /// BIP-381: Pay to Public Key Hash — `pkh(KEY)`.
87    Pkh(DescriptorKey),
88    /// BIP-382: Pay to Witness Public Key Hash — `wpkh(KEY)`.
89    Wpkh(DescriptorKey),
90    /// BIP-381: Pay to Script Hash wrapping wpkh — `sh(wpkh(KEY))`.
91    ShWpkh(DescriptorKey),
92    /// BIP-386: Pay to Taproot — `tr(KEY)` (key-path only).
93    Tr(DescriptorKey),
94    /// Raw script — `raw(HEX)`.
95    Raw(Vec<u8>),
96    /// OP_RETURN data — `raw(6a...)`.
97    OpReturn(Vec<u8>),
98}
99
100impl Descriptor {
101    /// Create a `pkh(KEY)` descriptor (BIP-381).
102    pub fn pkh(key: DescriptorKey) -> Self {
103        Descriptor::Pkh(key)
104    }
105
106    /// Create a `wpkh(KEY)` descriptor (BIP-382).
107    pub fn wpkh(key: DescriptorKey) -> Self {
108        Descriptor::Wpkh(key)
109    }
110
111    /// Create a `sh(wpkh(KEY))` descriptor (BIP-381).
112    pub fn sh_wpkh(key: DescriptorKey) -> Self {
113        Descriptor::ShWpkh(key)
114    }
115
116    /// Create a `tr(KEY)` descriptor (BIP-386, key-path only).
117    pub fn tr(key: DescriptorKey) -> Self {
118        Descriptor::Tr(key)
119    }
120
121    /// Compute the scriptPubKey for this descriptor.
122    pub fn script_pubkey(&self) -> Result<Vec<u8>, SignerError> {
123        match self {
124            Descriptor::Pkh(key) => {
125                let hash = key.hash160().ok_or(SignerError::ParseError(
126                    "pkh requires compressed key".into(),
127                ))?;
128                // OP_DUP OP_HASH160 OP_PUSH20 <hash> OP_EQUALVERIFY OP_CHECKSIG
129                let mut script = Vec::with_capacity(25);
130                script.push(0x76); // OP_DUP
131                script.push(0xa9); // OP_HASH160
132                script.push(0x14); // OP_PUSH20
133                script.extend_from_slice(&hash);
134                script.push(0x88); // OP_EQUALVERIFY
135                script.push(0xac); // OP_CHECKSIG
136                Ok(script)
137            }
138            Descriptor::Wpkh(key) => {
139                let hash = key.hash160().ok_or(SignerError::ParseError(
140                    "wpkh requires compressed key".into(),
141                ))?;
142                // OP_0 OP_PUSH20 <hash>
143                let mut script = Vec::with_capacity(22);
144                script.push(0x00); // OP_0
145                script.push(0x14); // OP_PUSH20
146                script.extend_from_slice(&hash);
147                Ok(script)
148            }
149            Descriptor::ShWpkh(key) => {
150                let hash = key.hash160().ok_or(SignerError::ParseError(
151                    "sh(wpkh) requires compressed key".into(),
152                ))?;
153                // Witness script: OP_0 OP_PUSH20 <hash>
154                let mut witness_script = Vec::with_capacity(22);
155                witness_script.push(0x00);
156                witness_script.push(0x14);
157                witness_script.extend_from_slice(&hash);
158                // P2SH: OP_HASH160 OP_PUSH20 HASH160(witness_script) OP_EQUAL
159                let script_hash = crypto::hash160(&witness_script);
160                let mut script = Vec::with_capacity(23);
161                script.push(0xa9); // OP_HASH160
162                script.push(0x14); // OP_PUSH20
163                script.extend_from_slice(&script_hash);
164                script.push(0x87); // OP_EQUAL
165                Ok(script)
166            }
167            Descriptor::Tr(key) => {
168                let xonly = match key {
169                    DescriptorKey::XOnly(k) => *k,
170                    DescriptorKey::Compressed(k) => {
171                        let mut xonly = [0u8; 32];
172                        xonly.copy_from_slice(&k[1..]);
173                        xonly
174                    }
175                };
176                let (output_key, _parity) = super::taproot::taproot_tweak(&xonly, None)?;
177                // OP_1 OP_PUSH32 <x_only_key>
178                let mut script = Vec::with_capacity(34);
179                script.push(0x51); // OP_1
180                script.push(0x20); // OP_PUSH32
181                script.extend_from_slice(&output_key);
182                Ok(script)
183            }
184            Descriptor::Raw(script) => Ok(script.clone()),
185            Descriptor::OpReturn(data) => {
186                let mut script = Vec::with_capacity(6 + data.len());
187                script.push(0x6a); // OP_RETURN
188                if data.len() <= 75 {
189                    script.push(data.len() as u8);
190                } else if data.len() <= 0xFF {
191                    script.push(0x4c); // OP_PUSHDATA1
192                    script.push(data.len() as u8);
193                } else if data.len() <= 0xFFFF {
194                    script.push(0x4d); // OP_PUSHDATA2
195                    script.extend_from_slice(&(data.len() as u16).to_le_bytes());
196                } else if data.len() <= 0xFFFF_FFFF {
197                    script.push(0x4e); // OP_PUSHDATA4
198                    script.extend_from_slice(&(data.len() as u32).to_le_bytes());
199                } else {
200                    return Err(SignerError::EncodingError(
201                        "op_return data length exceeds script push limit".into(),
202                    ));
203                }
204                script.extend_from_slice(data);
205                Ok(script)
206            }
207        }
208    }
209
210    /// Generate the Bitcoin address for this descriptor.
211    pub fn address(&self, hrp: &str) -> Result<String, SignerError> {
212        match self {
213            Descriptor::Pkh(key) => {
214                let hash = key.hash160().ok_or(SignerError::ParseError(
215                    "pkh requires compressed key".into(),
216                ))?;
217                let prefix = if hrp == "bc" || hrp == "mainnet" {
218                    0x00u8
219                } else {
220                    0x6Fu8
221                };
222                Ok(encoding::base58check_encode(prefix, &hash))
223            }
224            Descriptor::Wpkh(key) => {
225                let hash = key.hash160().ok_or(SignerError::ParseError(
226                    "wpkh requires compressed key".into(),
227                ))?;
228                encoding::bech32_encode(hrp, 0, &hash)
229            }
230            Descriptor::ShWpkh(_) => {
231                let script = self.script_pubkey()?;
232                let hash = &script[2..22];
233                let prefix = if hrp == "bc" || hrp == "mainnet" {
234                    0x05u8
235                } else {
236                    0xC4u8
237                };
238                Ok(encoding::base58check_encode(prefix, hash))
239            }
240            Descriptor::Tr(key) => {
241                let xonly = match key {
242                    DescriptorKey::XOnly(k) => *k,
243                    DescriptorKey::Compressed(k) => {
244                        let mut xo = [0u8; 32];
245                        xo.copy_from_slice(&k[1..]);
246                        xo
247                    }
248                };
249                let (output_key, _parity) = super::taproot::taproot_tweak(&xonly, None)?;
250                encoding::bech32_encode(hrp, 1, &output_key)
251            }
252            Descriptor::Raw(_) | Descriptor::OpReturn(_) => Err(SignerError::EncodingError(
253                "raw/op_return descriptors have no address".into(),
254            )),
255        }
256    }
257
258    /// Convert the descriptor to its string representation.
259    pub fn to_string_repr(&self) -> String {
260        match self {
261            Descriptor::Pkh(key) => format!("pkh({})", key_to_hex(key)),
262            Descriptor::Wpkh(key) => format!("wpkh({})", key_to_hex(key)),
263            Descriptor::ShWpkh(key) => format!("sh(wpkh({}))", key_to_hex(key)),
264            Descriptor::Tr(key) => format!("tr({})", key_to_hex(key)),
265            Descriptor::Raw(script) => format!("raw({})", hex::encode(script)),
266            Descriptor::OpReturn(data) => format!("raw(6a{})", hex::encode(data)),
267        }
268    }
269
270    /// Compute the descriptor checksum (BIP-380).
271    pub fn checksum(&self) -> String {
272        let desc_str = self.to_string_repr();
273        descriptor_checksum(&desc_str)
274    }
275
276    /// Get the full descriptor string with checksum.
277    pub fn to_string_with_checksum(&self) -> String {
278        let desc = self.to_string_repr();
279        let checksum = descriptor_checksum(&desc);
280        format!("{desc}#{checksum}")
281    }
282}
283
284// ─── Descriptor Parsing ─────────────────────────────────────────────
285
286/// Parse a descriptor string.
287///
288/// Supports `pkh(KEY)`, `wpkh(KEY)`, `sh(wpkh(KEY))`, `tr(KEY)`.
289pub fn parse(descriptor: &str) -> Result<Descriptor, SignerError> {
290    // Verify checksum when provided (split on the final '#').
291    let desc = if let Some((payload, checksum)) = descriptor.rsplit_once('#') {
292        if checksum.len() != 8 {
293            return Err(SignerError::ParseError(
294                "invalid descriptor checksum length".into(),
295            ));
296        }
297        let expected = descriptor_checksum(payload);
298        if checksum != expected {
299            return Err(SignerError::ParseError(
300                "invalid descriptor checksum".into(),
301            ));
302        }
303        payload
304    } else {
305        descriptor
306    };
307
308    if let Some(inner) = strip_wrapper(desc, "pkh(", ")") {
309        let key = DescriptorKey::from_hex(inner)?;
310        Ok(Descriptor::pkh(key))
311    } else if let Some(inner) = strip_wrapper(desc, "wpkh(", ")") {
312        let key = DescriptorKey::from_hex(inner)?;
313        Ok(Descriptor::wpkh(key))
314    } else if let Some(inner) = strip_wrapper(desc, "sh(wpkh(", "))") {
315        let key = DescriptorKey::from_hex(inner)?;
316        Ok(Descriptor::sh_wpkh(key))
317    } else if let Some(inner) = strip_wrapper(desc, "tr(", ")") {
318        let key = DescriptorKey::from_hex(inner)?;
319        Ok(Descriptor::tr(key))
320    } else if let Some(inner) = strip_wrapper(desc, "raw(", ")") {
321        let bytes = hex::decode(inner).map_err(|e| SignerError::ParseError(format!("hex: {e}")))?;
322        Ok(Descriptor::Raw(bytes))
323    } else {
324        Err(SignerError::ParseError(format!(
325            "unsupported descriptor: {desc}"
326        )))
327    }
328}
329
330/// Strip a prefix and suffix from a string, returning the inner content.
331fn strip_wrapper<'a>(s: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
332    s.strip_prefix(prefix).and_then(|s| s.strip_suffix(suffix))
333}
334
335// ─── Checksum (BIP-380) ────────────────────────────────────────────
336
337/// Compute the BIP-380 descriptor checksum.
338///
339/// Uses a modified polymod with the character set from BIP-380.
340fn descriptor_checksum(desc: &str) -> String {
341    const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
342    const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
343
344    fn polymod(c: u64, val: u64) -> u64 {
345        let c0 = c >> 35;
346        let mut c = ((c & 0x7FFFFFFFF) << 5) ^ val;
347        if c0 & 1 != 0 {
348            c ^= 0xf5dee51989;
349        }
350        if c0 & 2 != 0 {
351            c ^= 0xa9fdca3312;
352        }
353        if c0 & 4 != 0 {
354            c ^= 0x1bab10e32d;
355        }
356        if c0 & 8 != 0 {
357            c ^= 0x3706b1677a;
358        }
359        if c0 & 16 != 0 {
360            c ^= 0x644d626ffd;
361        }
362        c
363    }
364
365    let mut c = 1u64;
366    let mut cls = 0u64;
367    let mut clscount = 0u64;
368
369    for ch in desc.chars() {
370        if let Some(pos) = INPUT_CHARSET.find(ch) {
371            c = polymod(c, pos as u64 & 31);
372            cls = cls * 3 + (pos as u64 >> 5);
373            clscount += 1;
374            if clscount == 3 {
375                c = polymod(c, cls);
376                cls = 0;
377                clscount = 0;
378            }
379        }
380    }
381    if clscount > 0 {
382        c = polymod(c, cls);
383    }
384    for _ in 0..8 {
385        c = polymod(c, 0);
386    }
387    c ^= 1;
388
389    let mut result = String::with_capacity(8);
390    for j in 0..8 {
391        result.push(CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize] as char);
392    }
393    result
394}
395
396// ─── Helpers ────────────────────────────────────────────────────────
397
398fn key_to_hex(key: &DescriptorKey) -> String {
399    match key {
400        DescriptorKey::Compressed(k) => hex::encode(k),
401        DescriptorKey::XOnly(k) => hex::encode(k),
402    }
403}
404
405// ─── Tests ──────────────────────────────────────────────────────────
406
407#[cfg(test)]
408#[allow(clippy::unwrap_used, clippy::expect_used)]
409mod tests {
410    use super::*;
411
412    const TEST_PUBKEY: &str = "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798";
413
414    fn test_key() -> DescriptorKey {
415        DescriptorKey::from_hex(TEST_PUBKEY).expect("valid key")
416    }
417
418    #[test]
419    fn test_descriptor_key_from_hex_compressed() {
420        let key = test_key();
421        assert!(key.compressed_bytes().is_some());
422        assert!(key.x_only_bytes().is_none());
423    }
424
425    #[test]
426    fn test_descriptor_key_from_hex_xonly() {
427        let key = DescriptorKey::from_hex(
428            "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
429        )
430        .expect("valid");
431        assert!(key.x_only_bytes().is_some());
432    }
433
434    #[test]
435    fn test_descriptor_key_from_hex_invalid() {
436        assert!(DescriptorKey::from_hex("0102").is_err());
437        assert!(DescriptorKey::from_hex("invalid").is_err());
438    }
439
440    #[test]
441    fn test_descriptor_key_hash160() {
442        let key = test_key();
443        let h = key.hash160().expect("compressed key");
444        assert_eq!(h.len(), 20);
445        // Generator point HASH160 is well-known
446        assert_eq!(hex::encode(h), "751e76e8199196d454941c45d1b3a323f1433bd6");
447    }
448
449    #[test]
450    fn test_descriptor_pkh_script_pubkey() {
451        let key = test_key();
452        let desc = Descriptor::pkh(key);
453        let script = desc.script_pubkey().expect("ok");
454        assert_eq!(script.len(), 25);
455        assert_eq!(script[0], 0x76); // OP_DUP
456        assert_eq!(script[1], 0xa9); // OP_HASH160
457        assert_eq!(script[2], 0x14); // OP_PUSH20
458        assert_eq!(script[23], 0x88); // OP_EQUALVERIFY
459        assert_eq!(script[24], 0xac); // OP_CHECKSIG
460    }
461
462    #[test]
463    fn test_descriptor_wpkh_script_pubkey() {
464        let key = test_key();
465        let desc = Descriptor::wpkh(key);
466        let script = desc.script_pubkey().expect("ok");
467        assert_eq!(script.len(), 22);
468        assert_eq!(script[0], 0x00); // OP_0
469        assert_eq!(script[1], 0x14); // OP_PUSH20
470    }
471
472    #[test]
473    fn test_descriptor_sh_wpkh_script_pubkey() {
474        let key = test_key();
475        let desc = Descriptor::sh_wpkh(key);
476        let script = desc.script_pubkey().expect("ok");
477        assert_eq!(script.len(), 23);
478        assert_eq!(script[0], 0xa9); // OP_HASH160
479        assert_eq!(script[1], 0x14); // OP_PUSH20
480        assert_eq!(script[22], 0x87); // OP_EQUAL
481    }
482
483    #[test]
484    fn test_descriptor_tr_script_pubkey() {
485        let key = DescriptorKey::from_hex(
486            "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
487        )
488        .expect("valid");
489        let desc = Descriptor::tr(key);
490        let script = desc.script_pubkey().expect("ok");
491        assert_eq!(script.len(), 34);
492        assert_eq!(script[0], 0x51); // OP_1
493        assert_eq!(script[1], 0x20); // OP_PUSH32
494    }
495
496    #[test]
497    fn test_descriptor_tr_script_pubkey_uses_tweaked_output_key() {
498        let xonly = [
499            0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87,
500            0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B,
501            0x16, 0xF8, 0x17, 0x98,
502        ];
503        let desc = Descriptor::tr(DescriptorKey::XOnly(xonly));
504        let script = desc.script_pubkey().expect("ok");
505        let (tweaked, _) = crate::bitcoin::taproot::taproot_tweak(&xonly, None).expect("tweak");
506        assert_eq!(&script[2..], &tweaked);
507        assert_ne!(&script[2..], &xonly);
508    }
509
510    #[test]
511    fn test_descriptor_pkh_address_mainnet() {
512        let key = test_key();
513        let desc = Descriptor::pkh(key);
514        let addr = desc.address("bc").expect("ok");
515        // Generator point P2PKH address
516        assert_eq!(addr, "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH");
517    }
518
519    #[test]
520    fn test_descriptor_wpkh_address_mainnet() {
521        let key = test_key();
522        let desc = Descriptor::wpkh(key);
523        let addr = desc.address("bc").expect("ok");
524        assert!(addr.starts_with("bc1q"));
525        // Generator point P2WPKH address
526        assert_eq!(addr, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
527    }
528
529    #[test]
530    fn test_descriptor_sh_wpkh_address() {
531        let key = test_key();
532        let desc = Descriptor::sh_wpkh(key);
533        let addr = desc.address("bc").expect("ok");
534        assert!(addr.starts_with('3'));
535    }
536
537    #[test]
538    fn test_descriptor_tr_address() {
539        let key = DescriptorKey::from_hex(
540            "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
541        )
542        .expect("valid");
543        let desc = Descriptor::tr(key);
544        let addr = desc.address("bc").expect("ok");
545        assert!(addr.starts_with("bc1p"));
546        let xonly = [
547            0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87,
548            0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B,
549            0x16, 0xF8, 0x17, 0x98,
550        ];
551        let expected = crate::bitcoin::taproot::taproot_address(&xonly, None, "bc").expect("addr");
552        assert_eq!(addr, expected);
553    }
554
555    #[test]
556    fn test_descriptor_parse_pkh() {
557        let desc = parse(&format!("pkh({TEST_PUBKEY})")).expect("ok");
558        assert!(matches!(desc, Descriptor::Pkh(_)));
559    }
560
561    #[test]
562    fn test_descriptor_parse_wpkh() {
563        let desc = parse(&format!("wpkh({TEST_PUBKEY})")).expect("ok");
564        assert!(matches!(desc, Descriptor::Wpkh(_)));
565    }
566
567    #[test]
568    fn test_descriptor_parse_sh_wpkh() {
569        let desc = parse(&format!("sh(wpkh({TEST_PUBKEY}))")).expect("ok");
570        assert!(matches!(desc, Descriptor::ShWpkh(_)));
571    }
572
573    #[test]
574    fn test_descriptor_parse_tr() {
575        let xonly = "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798";
576        let desc = parse(&format!("tr({xonly})")).expect("ok");
577        assert!(matches!(desc, Descriptor::Tr(_)));
578    }
579
580    #[test]
581    fn test_descriptor_parse_raw() {
582        let desc = parse("raw(6a0568656c6c6f)").expect("ok");
583        assert!(matches!(desc, Descriptor::Raw(_)));
584    }
585
586    #[test]
587    fn test_descriptor_parse_with_checksum() {
588        let payload = format!("pkh({TEST_PUBKEY})");
589        let desc_str = format!("{payload}#{}", descriptor_checksum(&payload));
590        let desc = parse(&desc_str).expect("ok");
591        assert!(matches!(desc, Descriptor::Pkh(_)));
592    }
593
594    #[test]
595    fn test_descriptor_parse_with_invalid_checksum_fails() {
596        let desc_str = format!("pkh({TEST_PUBKEY})#aaaaaaaa");
597        assert!(parse(&desc_str).is_err());
598    }
599
600    #[test]
601    fn test_descriptor_parse_with_short_checksum_fails() {
602        let desc_str = format!("pkh({TEST_PUBKEY})#abc");
603        assert!(parse(&desc_str).is_err());
604    }
605
606    #[test]
607    fn test_descriptor_parse_invalid() {
608        assert!(parse("unknown(key)").is_err());
609    }
610
611    #[test]
612    fn test_descriptor_to_string_roundtrip() {
613        let key = test_key();
614        let desc = Descriptor::pkh(key);
615        let s = desc.to_string_repr();
616        let reparsed = parse(&s).expect("roundtrip");
617        assert!(matches!(reparsed, Descriptor::Pkh(_)));
618    }
619
620    #[test]
621    fn test_descriptor_checksum_length() {
622        let key = test_key();
623        let desc = Descriptor::pkh(key);
624        let cs = desc.checksum();
625        assert_eq!(cs.len(), 8);
626        assert!(cs.chars().all(|c| c.is_alphanumeric()));
627    }
628
629    #[test]
630    fn test_descriptor_with_checksum() {
631        let key = test_key();
632        let desc = Descriptor::wpkh(key);
633        let full = desc.to_string_with_checksum();
634        assert!(full.contains('#'));
635        let parts: Vec<&str> = full.split('#').collect();
636        assert_eq!(parts.len(), 2);
637        assert_eq!(parts[1].len(), 8);
638    }
639
640    #[test]
641    fn test_descriptor_testnet_address() {
642        let key = test_key();
643        let mainnet = Descriptor::pkh(key.clone()).address("bc").expect("ok");
644        let testnet = Descriptor::pkh(key).address("tb").expect("ok");
645        assert_ne!(mainnet, testnet);
646        assert!(testnet.starts_with('m') || testnet.starts_with('n'));
647    }
648
649    #[test]
650    fn test_descriptor_op_return() {
651        let data = vec![0x01, 0x02, 0x03];
652        let desc = Descriptor::OpReturn(data);
653        let script = desc.script_pubkey().expect("ok");
654        assert_eq!(script[0], 0x6a); // OP_RETURN
655        assert!(desc.address("bc").is_err()); // no address for OP_RETURN
656    }
657
658    #[test]
659    fn test_descriptor_op_return_pushdata1() {
660        let data = vec![0xAA; 80];
661        let desc = Descriptor::OpReturn(data.clone());
662        let script = desc.script_pubkey().expect("ok");
663        assert_eq!(script[0], 0x6a); // OP_RETURN
664        assert_eq!(script[1], 0x4c); // OP_PUSHDATA1
665        assert_eq!(script[2], 80);
666        assert_eq!(&script[3..], &data);
667    }
668
669    #[test]
670    fn test_descriptor_op_return_pushdata2() {
671        let data = vec![0x55; 300];
672        let desc = Descriptor::OpReturn(data.clone());
673        let script = desc.script_pubkey().expect("ok");
674        assert_eq!(script[0], 0x6a); // OP_RETURN
675        assert_eq!(script[1], 0x4d); // OP_PUSHDATA2
676        assert_eq!(&script[2..4], &300u16.to_le_bytes());
677        assert_eq!(&script[4..], &data);
678    }
679
680    #[test]
681    fn test_descriptor_xonly_key_no_hash160() {
682        let key = DescriptorKey::from_hex(
683            "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
684        )
685        .expect("valid");
686        assert!(key.hash160().is_none()); // x-only keys don't support HASH160
687        assert!(Descriptor::pkh(key).script_pubkey().is_err()); // pkh with x-only should error
688    }
689}