rgb/
descriptor.rs

1// RGB API library for smart contracts on Bitcoin & Lightning network
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2019-2023 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2023 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22use std::collections::{BTreeMap, BTreeSet};
23use std::fmt::{self, Display, Formatter};
24
25use amplify::Wrapper;
26#[cfg(feature = "bp")]
27use bpwallet::IdxBase;
28use psrgbt::Terminal;
29use rgbstd::bitcoin::key::UntweakedPublicKey;
30use rgbstd::bitcoin::PublicKey;
31use rgbstd::rgbcore::seals::txout::CloseMethod;
32use rgbstd::tapret::TapretCommitment;
33
34pub trait DescriptorRgb {
35    fn close_method(&self) -> CloseMethod;
36    fn add_tapret_tweak(&mut self, terminal: Terminal, tweak: TapretCommitment);
37}
38
39#[derive(Clone, Eq, PartialEq, Debug)]
40#[cfg_attr(
41    feature = "serde",
42    derive(Serialize, Deserialize),
43    serde(crate = "serde_crate", rename_all = "camelCase")
44)]
45pub struct TapretKey<K = UntweakedPublicKey> {
46    pub tr: K,
47    pub tweaks: BTreeMap<Terminal, BTreeSet<TapretCommitment>>,
48}
49
50impl<K: Display> Display for TapretKey<K> {
51    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
52        write!(f, "tapret({},tweaks(", self.tr)?;
53        let mut iter = self.tweaks.iter().peekable();
54        while let Some((term, tweaks)) = iter.next() {
55            write!(f, "{}/{}=", term.keychain, term.index)?;
56            let mut commitment_iter = tweaks.iter().peekable();
57            while let Some(tweak) = commitment_iter.next() {
58                write!(f, "{tweak}")?;
59                if commitment_iter.peek().is_some() {
60                    f.write_str(",")?;
61                }
62            }
63            if iter.peek().is_some() {
64                f.write_str(";")?;
65            }
66        }
67        f.write_str("))")
68    }
69}
70
71impl<K> TapretKey<K> {
72    pub fn with_key(key: K) -> Self {
73        TapretKey {
74            tr: key,
75            tweaks: empty!(),
76        }
77    }
78}
79
80impl<K> DescriptorRgb for TapretKey<K> {
81    fn close_method(&self) -> CloseMethod { CloseMethod::TapretFirst }
82
83    fn add_tapret_tweak(&mut self, terminal: Terminal, tweak: TapretCommitment) {
84        self.tweaks.entry(terminal).or_default().insert(tweak);
85    }
86}
87
88#[derive(Clone, Eq, PartialEq, Debug)]
89#[cfg_attr(
90    feature = "serde",
91    derive(Serialize, Deserialize),
92    serde(crate = "serde_crate", rename_all = "camelCase")
93)]
94pub struct WpkhDescr<K = PublicKey>(K);
95
96impl<K: Display> Display for WpkhDescr<K> {
97    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "wpkh({})", self.0) }
98}
99
100impl<K> WpkhDescr<K> {
101    pub fn with_key(key: K) -> Self { WpkhDescr(key) }
102}
103
104#[derive(Clone, Eq, PartialEq, Debug)]
105#[cfg_attr(
106    feature = "serde",
107    derive(Serialize, Deserialize),
108    serde(crate = "serde_crate", rename_all = "camelCase",)
109)]
110#[non_exhaustive]
111pub enum RgbDescr<K> {
112    Wpkh(WpkhDescr<K>),
113    TapretKey(TapretKey<K>),
114}
115
116impl<K> From<WpkhDescr<K>> for RgbDescr<K> {
117    fn from(wpkh: WpkhDescr<K>) -> Self { RgbDescr::Wpkh(wpkh) }
118}
119
120impl<K> From<TapretKey<K>> for RgbDescr<K> {
121    fn from(tapret: TapretKey<K>) -> Self { RgbDescr::TapretKey(tapret) }
122}
123
124impl<K: Display> Display for RgbDescr<K> {
125    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
126        match self {
127            RgbDescr::Wpkh(wpkh) => Display::fmt(wpkh, f),
128            RgbDescr::TapretKey(tapret_key) => Display::fmt(tapret_key, f),
129        }
130    }
131}
132
133impl<K> DescriptorRgb for RgbDescr<K> {
134    fn close_method(&self) -> CloseMethod {
135        match self {
136            RgbDescr::Wpkh(_) => CloseMethod::OpretFirst,
137            RgbDescr::TapretKey(d) => d.close_method(),
138        }
139    }
140
141    fn add_tapret_tweak(&mut self, terminal: Terminal, tweak: TapretCommitment) {
142        match self {
143            RgbDescr::Wpkh(_) => panic!("adding tapret tweak to non-taproot descriptor"),
144            RgbDescr::TapretKey(d) => d.add_tapret_tweak(terminal, tweak),
145        }
146    }
147}
148
149#[cfg(feature = "bp")]
150pub mod bp_wallet_integration {
151    use std::collections::{BTreeSet, HashMap};
152    use std::iter;
153
154    use bpwallet::{
155        Derive, DeriveXOnly, DerivedScript, Descriptor, KeyOrigin, Keychain, LegacyKeySig,
156        LegacyPk, NormalIndex, SigScript, SpkClass, TapDerivation, TapScript, TapTree,
157        TaprootKeySig, TrKey, Witness, Wpkh, XOnlyPk, XpubAccount, XpubDerivable,
158    };
159    use indexmap::IndexMap;
160
161    use super::*;
162
163    impl<K> From<TrKey<K>> for TapretKey<K>
164    where K: Clone + DeriveXOnly
165    {
166        fn from(tr_key: TrKey<K>) -> Self {
167            let tr = tr_key
168                .keys()
169                .next()
170                .cloned()
171                .expect("TrKey should have a key");
172            TapretKey {
173                tr,
174                tweaks: empty!(),
175            }
176        }
177    }
178
179    impl Descriptor<XpubDerivable> for RgbDescr<XpubDerivable> {
180        fn class(&self) -> SpkClass {
181            match self {
182                RgbDescr::Wpkh(_) => SpkClass::P2wpkh,
183                RgbDescr::TapretKey(_) => SpkClass::P2tr,
184            }
185        }
186
187        fn keys<'a>(&'a self) -> impl Iterator<Item = &'a XpubDerivable>
188        where XpubDerivable: 'a {
189            match self {
190                RgbDescr::Wpkh(wpkh) => {
191                    vec![&wpkh.0]
192                }
193                RgbDescr::TapretKey(tapret_key) => {
194                    vec![&tapret_key.tr]
195                }
196            }
197            .into_iter()
198        }
199
200        fn vars<'a>(&'a self) -> impl Iterator<Item = &'a ()>
201        where (): 'a {
202            iter::empty()
203        }
204
205        fn xpubs(&self) -> impl Iterator<Item = &XpubAccount> {
206            match self {
207                RgbDescr::Wpkh(d) => vec![d.0.spec()],
208                RgbDescr::TapretKey(d) => vec![d.tr.spec()],
209            }
210            .into_iter()
211        }
212
213        fn legacy_keyset(&self, terminal: bpwallet::Terminal) -> IndexMap<LegacyPk, KeyOrigin> {
214            match self {
215                RgbDescr::Wpkh(d) => Wpkh::from(d.0.clone()).legacy_keyset(terminal),
216                RgbDescr::TapretKey(_) => IndexMap::new(),
217            }
218        }
219
220        fn xonly_keyset(&self, terminal: bpwallet::Terminal) -> IndexMap<XOnlyPk, TapDerivation> {
221            match self {
222                RgbDescr::Wpkh(_) => IndexMap::new(),
223                RgbDescr::TapretKey(d) => TrKey::from(d.tr.clone()).xonly_keyset(terminal),
224            }
225        }
226
227        fn legacy_witness(
228            &self,
229            keysigs: HashMap<&KeyOrigin, LegacyKeySig>,
230        ) -> Option<(SigScript, Witness)> {
231            match self {
232                RgbDescr::Wpkh(d) => Wpkh::from(d.0.clone()).legacy_witness(keysigs),
233                RgbDescr::TapretKey(_) => None,
234            }
235        }
236
237        fn taproot_witness(&self, keysigs: HashMap<&KeyOrigin, TaprootKeySig>) -> Option<Witness> {
238            match self {
239                RgbDescr::Wpkh(_) => None,
240                RgbDescr::TapretKey(d) => TrKey::from(d.tr.clone()).taproot_witness(keysigs),
241            }
242        }
243    }
244
245    impl Derive<DerivedScript> for RgbDescr<XpubDerivable> {
246        fn default_keychain(&self) -> Keychain {
247            match self {
248                RgbDescr::Wpkh(d) => Wpkh::from(d.0.clone()).default_keychain(),
249                RgbDescr::TapretKey(d) => TrKey::from(d.tr.clone()).default_keychain(),
250            }
251        }
252
253        fn keychains(&self) -> BTreeSet<Keychain> {
254            match self {
255                RgbDescr::Wpkh(d) => Wpkh::from(d.0.clone()).keychains(),
256                RgbDescr::TapretKey(d) => TrKey::from(d.tr.clone()).keychains(),
257            }
258        }
259
260        fn derive(
261            &self,
262            keychain: impl Into<Keychain>,
263            index: impl Into<NormalIndex>,
264        ) -> impl Iterator<Item = DerivedScript> {
265            let keychain = keychain.into();
266            let index = index.into();
267
268            // collecting as a workaround for different opaque types
269            match self {
270                RgbDescr::Wpkh(d) => Wpkh::from(d.0.clone()).derive(keychain, index).collect(),
271                RgbDescr::TapretKey(d) => {
272                    let derivation = &d.tr;
273                    let terminal = Terminal::new(keychain.into_inner(), index.index());
274                    let derived_keys =
275                        <XpubDerivable as Derive<XOnlyPk>>::derive(derivation, keychain, index);
276                    let mut derived_scripts = Vec::with_capacity(d.tweaks.len() + 1);
277                    for internal_key in derived_keys {
278                        derived_scripts.push(DerivedScript::TaprootKeyOnly(internal_key.into()));
279                        for tweak in d.tweaks.get(&terminal).into_iter().flatten() {
280                            let commitment = tweak.commit();
281                            let tap_script = TapScript::from_unsafe(commitment.into_bytes());
282                            let tap_tree = TapTree::with_single_leaf(tap_script);
283                            let script =
284                                DerivedScript::TaprootScript(internal_key.into(), tap_tree);
285                            derived_scripts.push(script);
286                        }
287                    }
288                    derived_scripts
289                }
290            }
291            .into_iter()
292        }
293    }
294}
295
296#[cfg(feature = "bp")]
297#[cfg(test)]
298mod test {
299    use std::str::FromStr;
300
301    use bpwallet::{DeriveSet, Idx, Keychain, NormalIndex, Terminal, TrKey, XpubDerivable};
302    use rgbstd::rgbcore::commit_verify::mpc::Commitment;
303    use strict_types::StrictDumb;
304
305    use super::*;
306    use crate::DescriptorRgb;
307
308    #[test]
309    fn tapret_key_display() {
310        let xpub_str = "[643a7adc/86h/1h/0h]tpubDCNiWHaiSkgnQjuhsg9kjwaUzaxQjUcmhagvYzqQ3TYJTgFGJstVaqnu4yhtFktBhCVFmBNLQ5sN53qKzZbMksm3XEyGJsEhQPfVZdWmTE2/<0;1>/*";
311        let xpub = XpubDerivable::from_str(xpub_str).unwrap();
312        let internal_key: TrKey<<XpubDerivable as DeriveSet>::XOnly> = TrKey::from(xpub.clone());
313
314        // no tweaks
315        let mut tapret_key = TapretKey::from(internal_key);
316        assert_eq!(format!("{tapret_key}"), format!("tapret({xpub_str},tweaks())"));
317
318        // add a tweak to a new terminal
319        let terminal = Terminal::new(Keychain::INNER, NormalIndex::ZERO);
320        let tweak = TapretCommitment::with(Commitment::strict_dumb(), 2);
321        tapret_key.add_tapret_tweak(terminal.into(), tweak);
322        assert_eq!(
323            format!("{tapret_key}"),
324            format!("tapret({xpub_str},tweaks(1/0=00000000000000000000000000000000000000000s))")
325        );
326
327        // add another tweak to a new terminal
328        let terminal = Terminal::new(Keychain::from(7), NormalIndex::from(12u8));
329        let tweak = TapretCommitment::with(Commitment::strict_dumb(), 5);
330        tapret_key.add_tapret_tweak(terminal.into(), tweak.clone());
331        assert_eq!(
332            format!("{tapret_key}"),
333            format!(
334                "tapret({xpub_str},tweaks(1/0=00000000000000000000000000000000000000000s;7/\
335                 12=00000000000000000000000000000000000000001p))"
336            )
337        );
338
339        // add another tweak to an existing terminal
340        let tweak = TapretCommitment::with(Commitment::strict_dumb(), 2);
341        tapret_key.add_tapret_tweak(terminal.into(), tweak);
342        assert_eq!(
343            format!("{tapret_key}"),
344            format!(
345                "tapret({xpub_str},tweaks(1/0=00000000000000000000000000000000000000000s;7/\
346                 12=00000000000000000000000000000000000000000s,\
347                 00000000000000000000000000000000000000001p))"
348            )
349        );
350    }
351}