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                // OP_1 OP_PUSH32 <x_only_key>
177                let mut script = Vec::with_capacity(34);
178                script.push(0x51); // OP_1
179                script.push(0x20); // OP_PUSH32
180                script.extend_from_slice(&xonly);
181                Ok(script)
182            }
183            Descriptor::Raw(script) => Ok(script.clone()),
184            Descriptor::OpReturn(data) => {
185                let mut script = Vec::with_capacity(2 + data.len());
186                script.push(0x6a); // OP_RETURN
187                if data.len() <= 75 {
188                    script.push(data.len() as u8);
189                }
190                script.extend_from_slice(data);
191                Ok(script)
192            }
193        }
194    }
195
196    /// Generate the Bitcoin address for this descriptor.
197    pub fn address(&self, hrp: &str) -> Result<String, SignerError> {
198        match self {
199            Descriptor::Pkh(key) => {
200                let hash = key.hash160().ok_or(SignerError::ParseError(
201                    "pkh requires compressed key".into(),
202                ))?;
203                let prefix = if hrp == "bc" || hrp == "mainnet" {
204                    0x00u8
205                } else {
206                    0x6Fu8
207                };
208                Ok(encoding::base58check_encode(prefix, &hash))
209            }
210            Descriptor::Wpkh(key) => {
211                let hash = key.hash160().ok_or(SignerError::ParseError(
212                    "wpkh requires compressed key".into(),
213                ))?;
214                encoding::bech32_encode(hrp, 0, &hash)
215            }
216            Descriptor::ShWpkh(_) => {
217                let script = self.script_pubkey()?;
218                let hash = &script[2..22];
219                let prefix = if hrp == "bc" || hrp == "mainnet" {
220                    0x05u8
221                } else {
222                    0xC4u8
223                };
224                Ok(encoding::base58check_encode(prefix, hash))
225            }
226            Descriptor::Tr(key) => {
227                let xonly = match key {
228                    DescriptorKey::XOnly(k) => *k,
229                    DescriptorKey::Compressed(k) => {
230                        let mut xo = [0u8; 32];
231                        xo.copy_from_slice(&k[1..]);
232                        xo
233                    }
234                };
235                encoding::bech32_encode(hrp, 1, &xonly)
236            }
237            Descriptor::Raw(_) | Descriptor::OpReturn(_) => Err(SignerError::EncodingError(
238                "raw/op_return descriptors have no address".into(),
239            )),
240        }
241    }
242
243    /// Convert the descriptor to its string representation.
244    pub fn to_string_repr(&self) -> String {
245        match self {
246            Descriptor::Pkh(key) => format!("pkh({})", key_to_hex(key)),
247            Descriptor::Wpkh(key) => format!("wpkh({})", key_to_hex(key)),
248            Descriptor::ShWpkh(key) => format!("sh(wpkh({}))", key_to_hex(key)),
249            Descriptor::Tr(key) => format!("tr({})", key_to_hex(key)),
250            Descriptor::Raw(script) => format!("raw({})", hex::encode(script)),
251            Descriptor::OpReturn(data) => format!("raw(6a{})", hex::encode(data)),
252        }
253    }
254
255    /// Compute the descriptor checksum (BIP-380).
256    pub fn checksum(&self) -> String {
257        let desc_str = self.to_string_repr();
258        descriptor_checksum(&desc_str)
259    }
260
261    /// Get the full descriptor string with checksum.
262    pub fn to_string_with_checksum(&self) -> String {
263        let desc = self.to_string_repr();
264        let checksum = descriptor_checksum(&desc);
265        format!("{desc}#{checksum}")
266    }
267}
268
269// ─── Descriptor Parsing ─────────────────────────────────────────────
270
271/// Parse a descriptor string.
272///
273/// Supports `pkh(KEY)`, `wpkh(KEY)`, `sh(wpkh(KEY))`, `tr(KEY)`.
274pub fn parse(descriptor: &str) -> Result<Descriptor, SignerError> {
275    // Strip checksum
276    let desc = if let Some(pos) = descriptor.find('#') {
277        &descriptor[..pos]
278    } else {
279        descriptor
280    };
281
282    if let Some(inner) = strip_wrapper(desc, "pkh(", ")") {
283        let key = DescriptorKey::from_hex(inner)?;
284        Ok(Descriptor::pkh(key))
285    } else if let Some(inner) = strip_wrapper(desc, "wpkh(", ")") {
286        let key = DescriptorKey::from_hex(inner)?;
287        Ok(Descriptor::wpkh(key))
288    } else if let Some(inner) = strip_wrapper(desc, "sh(wpkh(", "))") {
289        let key = DescriptorKey::from_hex(inner)?;
290        Ok(Descriptor::sh_wpkh(key))
291    } else if let Some(inner) = strip_wrapper(desc, "tr(", ")") {
292        let key = DescriptorKey::from_hex(inner)?;
293        Ok(Descriptor::tr(key))
294    } else if let Some(inner) = strip_wrapper(desc, "raw(", ")") {
295        let bytes = hex::decode(inner).map_err(|e| SignerError::ParseError(format!("hex: {e}")))?;
296        Ok(Descriptor::Raw(bytes))
297    } else {
298        Err(SignerError::ParseError(format!(
299            "unsupported descriptor: {desc}"
300        )))
301    }
302}
303
304/// Strip a prefix and suffix from a string, returning the inner content.
305fn strip_wrapper<'a>(s: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
306    s.strip_prefix(prefix).and_then(|s| s.strip_suffix(suffix))
307}
308
309// ─── Checksum (BIP-380) ────────────────────────────────────────────
310
311/// Compute the BIP-380 descriptor checksum.
312///
313/// Uses a modified polymod with the character set from BIP-380.
314fn descriptor_checksum(desc: &str) -> String {
315    const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
316    const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
317
318    fn polymod(c: u64, val: u64) -> u64 {
319        let c0 = c >> 35;
320        let mut c = ((c & 0x7FFFFFFFF) << 5) ^ val;
321        if c0 & 1 != 0 {
322            c ^= 0xf5dee51989;
323        }
324        if c0 & 2 != 0 {
325            c ^= 0xa9fdca3312;
326        }
327        if c0 & 4 != 0 {
328            c ^= 0x1bab10e32d;
329        }
330        if c0 & 8 != 0 {
331            c ^= 0x3706b1677a;
332        }
333        if c0 & 16 != 0 {
334            c ^= 0x644d626ffd;
335        }
336        c
337    }
338
339    let mut c = 1u64;
340    let mut cls = 0u64;
341    let mut clscount = 0u64;
342
343    for ch in desc.chars() {
344        if let Some(pos) = INPUT_CHARSET.find(ch) {
345            c = polymod(c, pos as u64 & 31);
346            cls = cls * 3 + (pos as u64 >> 5);
347            clscount += 1;
348            if clscount == 3 {
349                c = polymod(c, cls);
350                cls = 0;
351                clscount = 0;
352            }
353        }
354    }
355    if clscount > 0 {
356        c = polymod(c, cls);
357    }
358    for _ in 0..8 {
359        c = polymod(c, 0);
360    }
361    c ^= 1;
362
363    let mut result = String::with_capacity(8);
364    for j in 0..8 {
365        result.push(CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize] as char);
366    }
367    result
368}
369
370// ─── Helpers ────────────────────────────────────────────────────────
371
372fn key_to_hex(key: &DescriptorKey) -> String {
373    match key {
374        DescriptorKey::Compressed(k) => hex::encode(k),
375        DescriptorKey::XOnly(k) => hex::encode(k),
376    }
377}
378
379// ─── Tests ──────────────────────────────────────────────────────────
380
381#[cfg(test)]
382#[allow(clippy::unwrap_used, clippy::expect_used)]
383mod tests {
384    use super::*;
385
386    const TEST_PUBKEY: &str = "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798";
387
388    fn test_key() -> DescriptorKey {
389        DescriptorKey::from_hex(TEST_PUBKEY).expect("valid key")
390    }
391
392    #[test]
393    fn test_descriptor_key_from_hex_compressed() {
394        let key = test_key();
395        assert!(key.compressed_bytes().is_some());
396        assert!(key.x_only_bytes().is_none());
397    }
398
399    #[test]
400    fn test_descriptor_key_from_hex_xonly() {
401        let key = DescriptorKey::from_hex(
402            "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
403        )
404        .expect("valid");
405        assert!(key.x_only_bytes().is_some());
406    }
407
408    #[test]
409    fn test_descriptor_key_from_hex_invalid() {
410        assert!(DescriptorKey::from_hex("0102").is_err());
411        assert!(DescriptorKey::from_hex("invalid").is_err());
412    }
413
414    #[test]
415    fn test_descriptor_key_hash160() {
416        let key = test_key();
417        let h = key.hash160().expect("compressed key");
418        assert_eq!(h.len(), 20);
419        // Generator point HASH160 is well-known
420        assert_eq!(hex::encode(h), "751e76e8199196d454941c45d1b3a323f1433bd6");
421    }
422
423    #[test]
424    fn test_descriptor_pkh_script_pubkey() {
425        let key = test_key();
426        let desc = Descriptor::pkh(key);
427        let script = desc.script_pubkey().expect("ok");
428        assert_eq!(script.len(), 25);
429        assert_eq!(script[0], 0x76); // OP_DUP
430        assert_eq!(script[1], 0xa9); // OP_HASH160
431        assert_eq!(script[2], 0x14); // OP_PUSH20
432        assert_eq!(script[23], 0x88); // OP_EQUALVERIFY
433        assert_eq!(script[24], 0xac); // OP_CHECKSIG
434    }
435
436    #[test]
437    fn test_descriptor_wpkh_script_pubkey() {
438        let key = test_key();
439        let desc = Descriptor::wpkh(key);
440        let script = desc.script_pubkey().expect("ok");
441        assert_eq!(script.len(), 22);
442        assert_eq!(script[0], 0x00); // OP_0
443        assert_eq!(script[1], 0x14); // OP_PUSH20
444    }
445
446    #[test]
447    fn test_descriptor_sh_wpkh_script_pubkey() {
448        let key = test_key();
449        let desc = Descriptor::sh_wpkh(key);
450        let script = desc.script_pubkey().expect("ok");
451        assert_eq!(script.len(), 23);
452        assert_eq!(script[0], 0xa9); // OP_HASH160
453        assert_eq!(script[1], 0x14); // OP_PUSH20
454        assert_eq!(script[22], 0x87); // OP_EQUAL
455    }
456
457    #[test]
458    fn test_descriptor_tr_script_pubkey() {
459        let key = DescriptorKey::from_hex(
460            "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
461        )
462        .expect("valid");
463        let desc = Descriptor::tr(key);
464        let script = desc.script_pubkey().expect("ok");
465        assert_eq!(script.len(), 34);
466        assert_eq!(script[0], 0x51); // OP_1
467        assert_eq!(script[1], 0x20); // OP_PUSH32
468    }
469
470    #[test]
471    fn test_descriptor_pkh_address_mainnet() {
472        let key = test_key();
473        let desc = Descriptor::pkh(key);
474        let addr = desc.address("bc").expect("ok");
475        // Generator point P2PKH address
476        assert_eq!(addr, "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH");
477    }
478
479    #[test]
480    fn test_descriptor_wpkh_address_mainnet() {
481        let key = test_key();
482        let desc = Descriptor::wpkh(key);
483        let addr = desc.address("bc").expect("ok");
484        assert!(addr.starts_with("bc1q"));
485        // Generator point P2WPKH address
486        assert_eq!(addr, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
487    }
488
489    #[test]
490    fn test_descriptor_sh_wpkh_address() {
491        let key = test_key();
492        let desc = Descriptor::sh_wpkh(key);
493        let addr = desc.address("bc").expect("ok");
494        assert!(addr.starts_with('3'));
495    }
496
497    #[test]
498    fn test_descriptor_tr_address() {
499        let key = DescriptorKey::from_hex(
500            "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
501        )
502        .expect("valid");
503        let desc = Descriptor::tr(key);
504        let addr = desc.address("bc").expect("ok");
505        assert!(addr.starts_with("bc1p"));
506    }
507
508    #[test]
509    fn test_descriptor_parse_pkh() {
510        let desc = parse(&format!("pkh({TEST_PUBKEY})")).expect("ok");
511        assert!(matches!(desc, Descriptor::Pkh(_)));
512    }
513
514    #[test]
515    fn test_descriptor_parse_wpkh() {
516        let desc = parse(&format!("wpkh({TEST_PUBKEY})")).expect("ok");
517        assert!(matches!(desc, Descriptor::Wpkh(_)));
518    }
519
520    #[test]
521    fn test_descriptor_parse_sh_wpkh() {
522        let desc = parse(&format!("sh(wpkh({TEST_PUBKEY}))")).expect("ok");
523        assert!(matches!(desc, Descriptor::ShWpkh(_)));
524    }
525
526    #[test]
527    fn test_descriptor_parse_tr() {
528        let xonly = "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798";
529        let desc = parse(&format!("tr({xonly})")).expect("ok");
530        assert!(matches!(desc, Descriptor::Tr(_)));
531    }
532
533    #[test]
534    fn test_descriptor_parse_raw() {
535        let desc = parse("raw(6a0568656c6c6f)").expect("ok");
536        assert!(matches!(desc, Descriptor::Raw(_)));
537    }
538
539    #[test]
540    fn test_descriptor_parse_with_checksum() {
541        let desc_str = format!("pkh({TEST_PUBKEY})#something");
542        let desc = parse(&desc_str).expect("ok");
543        assert!(matches!(desc, Descriptor::Pkh(_)));
544    }
545
546    #[test]
547    fn test_descriptor_parse_invalid() {
548        assert!(parse("unknown(key)").is_err());
549    }
550
551    #[test]
552    fn test_descriptor_to_string_roundtrip() {
553        let key = test_key();
554        let desc = Descriptor::pkh(key);
555        let s = desc.to_string_repr();
556        let reparsed = parse(&s).expect("roundtrip");
557        assert!(matches!(reparsed, Descriptor::Pkh(_)));
558    }
559
560    #[test]
561    fn test_descriptor_checksum_length() {
562        let key = test_key();
563        let desc = Descriptor::pkh(key);
564        let cs = desc.checksum();
565        assert_eq!(cs.len(), 8);
566        assert!(cs.chars().all(|c| c.is_alphanumeric()));
567    }
568
569    #[test]
570    fn test_descriptor_with_checksum() {
571        let key = test_key();
572        let desc = Descriptor::wpkh(key);
573        let full = desc.to_string_with_checksum();
574        assert!(full.contains('#'));
575        let parts: Vec<&str> = full.split('#').collect();
576        assert_eq!(parts.len(), 2);
577        assert_eq!(parts[1].len(), 8);
578    }
579
580    #[test]
581    fn test_descriptor_testnet_address() {
582        let key = test_key();
583        let mainnet = Descriptor::pkh(key.clone()).address("bc").expect("ok");
584        let testnet = Descriptor::pkh(key).address("tb").expect("ok");
585        assert_ne!(mainnet, testnet);
586        assert!(testnet.starts_with('m') || testnet.starts_with('n'));
587    }
588
589    #[test]
590    fn test_descriptor_op_return() {
591        let data = vec![0x01, 0x02, 0x03];
592        let desc = Descriptor::OpReturn(data);
593        let script = desc.script_pubkey().expect("ok");
594        assert_eq!(script[0], 0x6a); // OP_RETURN
595        assert!(desc.address("bc").is_err()); // no address for OP_RETURN
596    }
597
598    #[test]
599    fn test_descriptor_xonly_key_no_hash160() {
600        let key = DescriptorKey::from_hex(
601            "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
602        )
603        .expect("valid");
604        assert!(key.hash160().is_none()); // x-only keys don't support HASH160
605        assert!(Descriptor::pkh(key).script_pubkey().is_err()); // pkh with x-only should error
606    }
607}