descriptor_codec/
lib.rs

1// Written in 2025 by Joshua Doman <joshsdoman@gmail.com>
2// SPDX-License-Identifier: CC0-1.0
3
4//! # Descriptor Codec
5//!
6//! Efficiently encode and decode Bitcoin wallet descriptors with a 30-40% size reduction.
7//!
8//! ## Overview
9//!
10//! Bitcoin wallet descriptors encode the spending conditions for Bitcoin outputs, including
11//! keys, scripts, and other requirements. Descriptors are typically represented as human-readable
12//! strings, but this adds unnecessary overhead, which is not ideal for QR codes and other forms
13//! of machine-to-machine communication.
14//!
15//! This library efficiently encodes descriptors using tag-based and variable-length encoding,
16//! reducing the number of bytes by 30-40%. It supports all descriptors, including those with
17//! private keys.
18//!
19//! ## Usage
20//! ```rust
21//! use std::str::FromStr;
22//! use descriptor_codec::{encode, decode};
23//! use miniscript::descriptor::{Descriptor, DescriptorPublicKey};
24//!
25//! // Create a descriptor - a 2-of-3 multisig in this example
26//! let descriptor = "wsh(sortedmulti(2,\
27//!     03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,\
28//!     036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00,\
29//!     02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29\
30//! ))#hfj7wz7l";
31//!
32//! // Encode the descriptor
33//! let encoded_descriptor = encode(descriptor).unwrap();
34//!
35//! // Recover the original descriptor
36//! let decoded_descriptor = decode(&encoded_descriptor).unwrap();
37//! assert_eq!(descriptor.to_string(), decoded_descriptor);
38//! ```
39//!
40
41// Coding conventions
42#![deny(unsafe_code)]
43#![deny(non_upper_case_globals)]
44#![deny(non_camel_case_types)]
45#![deny(non_snake_case)]
46#![deny(unused_mut)]
47#![deny(dead_code)]
48#![deny(unused_imports)]
49#![deny(missing_docs)]
50
51#[cfg(not(any(feature = "std")))]
52compile_error!("`std` must be enabled");
53
54pub mod decoder;
55mod dummy;
56pub mod encoder;
57mod tag;
58mod test_helpers;
59mod varint;
60
61pub use decoder::Error;
62
63use bitcoin::{
64    hashes::{hash160, ripemd160, sha256},
65    secp256k1,
66};
67use miniscript::{
68    Descriptor, TranslatePk, Translator,
69    descriptor::{DescriptorPublicKey, DescriptorSecretKey, KeyMap},
70    hash256,
71};
72use std::collections::BTreeMap;
73use std::str::FromStr;
74
75/// Parses and encodes a Bitcoin descriptor
76pub fn encode(s: &str) -> Result<Vec<u8>, miniscript::Error> {
77    let secp = secp256k1::Secp256k1::new();
78    let (descriptor, key_map) = parse_descriptor(&secp, s)?;
79    let (mut template, mut payload) = encoder::encode(descriptor, &key_map);
80    template.append(&mut payload);
81    Ok(template)
82}
83
84/// Decodes a Bitcoin descriptor
85pub fn decode(bytes: &[u8]) -> Result<String, Error> {
86    let (_, _, size) = decoder::decode_template(bytes)?;
87    let (descriptor, key_map) = decoder::decode_with_payload(&bytes[..size], &bytes[size..])?;
88    Ok(descriptor.to_string_with_secret(&key_map))
89}
90
91/// Parse a descriptor that may contain secret keys
92///
93/// Internally turns every secret key found into the corresponding public key and then returns a
94/// a descriptor that only contains public keys and a map to lookup the secret key given a public key.
95///
96/// Re-implements `parse_descriptor` from `miniscript/descriptor` to handle MultiXPrivs by replacing
97/// each MultiXPriv with an indexed dummy SinglePub and adding the MultiXpriv to the key map.
98fn parse_descriptor<C: secp256k1::Signing>(
99    secp: &secp256k1::Secp256k1<C>,
100    s: &str,
101) -> Result<(Descriptor<DescriptorPublicKey>, KeyMap), miniscript::Error> {
102    fn parse_key<C: secp256k1::Signing>(
103        s: &str,
104        key_map: &mut KeyMap,
105        secp: &secp256k1::Secp256k1<C>,
106    ) -> Result<DescriptorPublicKey, miniscript::Error> {
107        let (public_key, secret_key) = match DescriptorSecretKey::from_str(s) {
108            Ok(sk) => (
109                sk.to_public(secp)
110                    .unwrap_or(test_helpers::create_dpk_single_compressed_no_origin(
111                        1 + key_map.len() as u32,
112                    )),
113                Some(sk),
114            ),
115            Err(_) => (
116                DescriptorPublicKey::from_str(s)
117                    .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?,
118                None,
119            ),
120        };
121
122        if let Some(secret_key) = secret_key {
123            key_map.insert(public_key.clone(), secret_key);
124        }
125
126        Ok(public_key)
127    }
128
129    let mut keymap_pk = KeyMapWrapper(BTreeMap::new(), secp);
130
131    struct KeyMapWrapper<'a, C: secp256k1::Signing>(KeyMap, &'a secp256k1::Secp256k1<C>);
132
133    impl<C: secp256k1::Signing> Translator<String, DescriptorPublicKey, miniscript::Error>
134        for KeyMapWrapper<'_, C>
135    {
136        fn pk(&mut self, pk: &String) -> Result<DescriptorPublicKey, miniscript::Error> {
137            parse_key(pk, &mut self.0, self.1)
138        }
139
140        fn sha256(&mut self, sha256: &String) -> Result<sha256::Hash, miniscript::Error> {
141            let hash = sha256::Hash::from_str(sha256)
142                .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
143            Ok(hash)
144        }
145
146        fn hash256(&mut self, hash256: &String) -> Result<hash256::Hash, miniscript::Error> {
147            let hash = hash256::Hash::from_str(hash256)
148                .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
149            Ok(hash)
150        }
151
152        fn ripemd160(&mut self, ripemd160: &String) -> Result<ripemd160::Hash, miniscript::Error> {
153            let hash = ripemd160::Hash::from_str(ripemd160)
154                .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
155            Ok(hash)
156        }
157
158        fn hash160(&mut self, hash160: &String) -> Result<hash160::Hash, miniscript::Error> {
159            let hash = hash160::Hash::from_str(hash160)
160                .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
161            Ok(hash)
162        }
163    }
164
165    let descriptor = Descriptor::<String>::from_str(s)?;
166    let descriptor = descriptor
167        .translate_pk(&mut keymap_pk)
168        .map_err(miniscript::TranslateErr::flatten)?;
169
170    Ok((descriptor, keymap_pk.0))
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_integration() {
179        let descriptors = vec![
180            "sh(sortedmulti(2,[2c49202a/45'/0'/0'/0]xpub6EigxozzGaNVWUwEFnbyX6oHPdpWTKgJgbfpRbAcdiGpGMrdpPinCoHBXehu35sqJHpgLDTxigAnFQG3opKjXQoSmGMrMNHz81ALZSBRCWw/0/*,[55b43a50/45'/0'/0'/0]xpub6EAtA5XJ6pwFQ7L32iAJMgiWQEcrwU75NNWQ6H6eavwznDFeGFzTbSFdDKNdbG2HQdZvzrXuCyEYSSJ4cGsmfoPkKUKQ6haNKMRqG4pD4xi/0/*,[35931b5e/0/0/0/0]xpub6EDykLBC5EfaDNC7Mpg2H8veCaJHDgxH2JQvRtxJrbyeAhXWV2jJzB9XL4jMiFN5TzQefYi4V4nDiH4bxhkrweQ3Smxc8uP4ux9HrMGV81P/0/*))#2esvpcaf",
181            "wsh(sortedmulti(2,[3abf21c8/48'/0'/0'/2']xpub6DYotmPf2kXFYhJMFDpfydjiXG1RzmH1V7Fnn2Z38DgN2oSYruczMyTFZZPz6yXq47Re8anhXWGj4yMzPTA3bjPDdpA96TLUbMehrH3sBna/<0;1>/*,[a1a4bd46/48'/0'/0'/2']xpub6DvXYo8BwnRACos42ME7tNL48JQhLMQ33ENfniLM9KZmeZGbBhyh1Jkfo3hUKmmjW92o3r7BprTPPdrTr4QLQR7aRnSBfz1UFMceW5ibhTc/<0;1>/*,[ed91913d/48'/0'/0'/2']xpub6EQUho4Z4pwh2UQGdPjoPrbtjd6qqseKZCEBLcZbJ7y6c9XBWHRkhERiADJfwRcUs14nQsxF3hvx7aFkbk3tfp4dnKfkcns217kBTVVN5gY/<0;1>/*))#e7m305nf",
182            "sh(wsh(sortedmulti(2,[2c49202a/45'/0'/0'/0]xpub6EigxozzGaNVWUwEFnbyX6oHPdpWTKgJgbfpRbAcdiGpGMrdpPinCoHBXehu35sqJHpgLDTxigAnFQG3opKjXQoSmGMrMNHz81ALZSBRCWw/0/*,[55b43a50/45'/0'/0'/0]xpub6EAtA5XJ6pwFQ7L32iAJMgiWQEcrwU75NNWQ6H6eavwznDFeGFzTbSFdDKNdbG2HQdZvzrXuCyEYSSJ4cGsmfoPkKUKQ6haNKMRqG4pD4xi/0/*,[35931b5e/0/0/0/0]xpub6EDykLBC5EfaDNC7Mpg2H8veCaJHDgxH2JQvRtxJrbyeAhXWV2jJzB9XL4jMiFN5TzQefYi4V4nDiH4bxhkrweQ3Smxc8uP4ux9HrMGV81P/0/*)))#c0t8r3nk",
183            "wsh(multi(2,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556,023e9be8b82c7469c88b1912a61611dffb9f65bbf5a176952727e0046513eca0de))#qdgya3w5",
184            "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)#8zl0zxma",
185            "sh(wsh(or_d(pk(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),and_v(v:pk(02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e),older(1000)))))#ky8du3e6",
186            "wsh(thresh(4,pk([7258e4f9/44'/1'/0']tpubDCZrkQoEU3845aFKUu9VQBYWZtrTwxMzcxnBwKFCYXHD6gEXvtFcxddCCLFsEwmxQaG15izcHxj48SXg1QS5FQGMBx5Ak6deXKPAL7wauBU/0/*),s:pk([c80b1469/44'/1'/0']tpubDD3UwwHoNUF4F3Vi5PiUVTc3ji1uThuRfFyBexTSHoAcHuWW2z8qEE2YujegcLtgthr3wMp3ZauvNG9eT9xfJyxXCfNty8h6rDBYU8UU1qq/0/*),s:pk([4e5024fe/44'/1'/0']tpubDDLrpPymPLSCJyCMLQdmcWxrAWwsqqssm5NdxT2WSdEBPSXNXxwbeKtsHAyXPpLkhUyKovtZgCi47QxVpw9iVkg95UUgeevyAqtJ9dqBqa1/0/*),s:pk([3b1d1ee9/44'/1'/0']tpubDCmDTANBWPzf6d8Ap1J5Ku7J1Ay92MpHMrEV7M5muWxCrTBN1g5f1NPcjMEL6dJHxbvEKNZtYCdowaSTN81DAyLsmv6w6xjJHCQNkxrsrfu/0/*),sln:after(840000),sln:after(1050000),sln:after(1260000)))#fk029528",
187            "tr(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5,{pk(fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),pk(e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)})#2rqrdjrh",
188            "pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/0)#m6s0eyht",
189            "pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<2147483647';0>/0)#s0hk8xf9",
190        ];
191
192        for desc_str in descriptors {
193            assert_eq!(desc_str, decode(&encode(desc_str).unwrap()).unwrap());
194        }
195    }
196}