bells_miniscript/miniscript/
analyzable.rs

1// Written in 2018 by Andrew Poelstra <apoelstra@wpsoftware.net>
2// SPDX-License-Identifier: CC0-1.0
3
4//!  Miniscript Analysis
5//!
6//! Tools for determining whether the guarantees offered by the library
7//! actually hold.
8
9use core::fmt;
10#[cfg(feature = "std")]
11use std::error;
12
13use crate::prelude::*;
14use crate::{Miniscript, MiniscriptKey, ScriptContext, Terminal};
15
16/// Params for parsing miniscripts that either non-sane or non-specified(experimental) in the spec.
17/// Used as a parameter [`Miniscript::from_str_ext`] and [`Miniscript::parse_with_ext`].
18///
19/// This allows parsing miniscripts if
20/// 1. It is unsafe(does not require a digital signature to spend it)
21/// 2. It contains a unspendable path because of either
22///     a. Resource limitations
23///     b. Timelock Mixing
24/// 3. The script is malleable and thereby some of satisfaction weight
25///    guarantees are not satisfied.
26/// 4. It has repeated public keys
27/// 5. raw pkh fragments without the pk. This could be obtained when parsing miniscript from script
28#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)]
29pub struct ExtParams {
30    /// Allow parsing of non-safe miniscripts
31    pub top_unsafe: bool,
32    /// Allow parsing of miniscripts with unspendable paths
33    pub resource_limitations: bool,
34    /// Allow parsing of miniscripts with timelock mixing
35    pub timelock_mixing: bool,
36    /// Allow parsing of malleable miniscripts
37    pub malleability: bool,
38    /// Allow parsing of miniscripts with repeated public keys
39    pub repeated_pk: bool,
40    /// Allow parsing of miniscripts with raw pkh fragments without the pk.
41    /// This could be obtained when parsing miniscript from script
42    pub raw_pkh: bool,
43}
44
45impl ExtParams {
46    /// Create a new ExtParams that with all the sanity rules
47    pub fn new() -> ExtParams {
48        ExtParams {
49            top_unsafe: false,
50            resource_limitations: false,
51            timelock_mixing: false,
52            malleability: false,
53            repeated_pk: false,
54            raw_pkh: false,
55        }
56    }
57
58    /// Create a new ExtParams that allows all the sanity rules
59    pub fn sane() -> ExtParams {
60        ExtParams::new()
61    }
62
63    /// Create a new ExtParams that insanity rules
64    /// This enables parsing well specified but "insane" miniscripts.
65    /// Refer to the [`ExtParams`] documentation for more details on "insane" miniscripts.
66    pub fn insane() -> ExtParams {
67        ExtParams {
68            top_unsafe: true,
69            resource_limitations: true,
70            timelock_mixing: true,
71            malleability: true,
72            repeated_pk: true,
73            raw_pkh: false,
74        }
75    }
76
77    /// Enable all non-sane rules and experimental rules
78    pub fn allow_all() -> ExtParams {
79        ExtParams {
80            top_unsafe: true,
81            resource_limitations: true,
82            timelock_mixing: true,
83            malleability: true,
84            repeated_pk: true,
85            raw_pkh: true,
86        }
87    }
88
89    /// Builder that allows non-safe miniscripts.
90    pub fn top_unsafe(mut self) -> ExtParams {
91        self.top_unsafe = true;
92        self
93    }
94
95    /// Builder that allows miniscripts with exceed resource limitations.
96    pub fn exceed_resource_limitations(mut self) -> ExtParams {
97        self.resource_limitations = true;
98        self
99    }
100
101    /// Builder that allows miniscripts with timelock mixing.
102    pub fn timelock_mixing(mut self) -> ExtParams {
103        self.timelock_mixing = true;
104        self
105    }
106
107    /// Builder that allows malleable miniscripts.
108    pub fn malleability(mut self) -> ExtParams {
109        self.malleability = true;
110        self
111    }
112
113    /// Builder that allows miniscripts with repeated public keys.
114    pub fn repeated_pk(mut self) -> ExtParams {
115        self.repeated_pk = true;
116        self
117    }
118
119    /// Builder that allows miniscripts with raw pkh fragments.
120    pub fn raw_pkh(mut self) -> ExtParams {
121        self.raw_pkh = true;
122        self
123    }
124}
125
126/// Possible reasons Miniscript guarantees can fail
127/// We currently mark Miniscript as Non-Analyzable if
128/// 1. It is unsafe(does not require a digital signature to spend it)
129/// 2. It contains a unspendable path because of either
130///     a. Resource limitations
131///     b. Timelock Mixing
132/// 3. The script is malleable and thereby some of satisfaction weight
133///    guarantees are not satisfied.
134/// 4. It has repeated publickeys
135#[derive(Debug, PartialEq)]
136pub enum AnalysisError {
137    /// Top level is not safe.
138    SiglessBranch,
139    /// Repeated Pubkeys
140    RepeatedPubkeys,
141    /// Miniscript contains at least one path that exceeds resource limits
142    BranchExceedResouceLimits,
143    /// Contains a combination of heightlock and timelock
144    HeightTimelockCombination,
145    /// Malleable script
146    Malleable,
147    /// Contains partial descriptor raw pkh
148    ContainsRawPkh,
149}
150
151impl fmt::Display for AnalysisError {
152    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
153        match *self {
154            AnalysisError::SiglessBranch => {
155                f.write_str("All spend paths must require a signature")
156            }
157            AnalysisError::RepeatedPubkeys => {
158                f.write_str("Miniscript contains repeated pubkeys or pubkeyhashes")
159            }
160            AnalysisError::BranchExceedResouceLimits => {
161                f.write_str("At least one spend path exceeds the resource limits(stack depth/satisfaction size..)")
162            }
163            AnalysisError::HeightTimelockCombination => {
164                f.write_str("Contains a combination of heightlock and timelock")
165            }
166            AnalysisError::Malleable => f.write_str("Miniscript is malleable"),
167            AnalysisError::ContainsRawPkh => f.write_str("Miniscript contains raw pkh"),
168        }
169    }
170}
171
172#[cfg(feature = "std")]
173impl error::Error for AnalysisError {
174    fn cause(&self) -> Option<&dyn error::Error> {
175        use self::AnalysisError::*;
176
177        match self {
178            SiglessBranch
179            | RepeatedPubkeys
180            | BranchExceedResouceLimits
181            | HeightTimelockCombination
182            | Malleable
183            | ContainsRawPkh => None,
184        }
185    }
186}
187
188impl<Pk: MiniscriptKey, Ctx: ScriptContext> Miniscript<Pk, Ctx> {
189    /// Whether all spend paths of miniscript require a signature
190    pub fn requires_sig(&self) -> bool {
191        self.ty.mall.safe
192    }
193
194    /// Whether the miniscript is malleable
195    pub fn is_non_malleable(&self) -> bool {
196        self.ty.mall.non_malleable
197    }
198
199    /// Whether the miniscript can exceed the resource limits(Opcodes, Stack limit etc)
200    // It maybe possible to return a detail error type containing why the miniscript
201    // failed. But doing so may require returning a collection of errors
202    pub fn within_resource_limits(&self) -> bool {
203        Ctx::check_local_validity(self).is_ok()
204    }
205
206    /// Whether the miniscript contains a combination of timelocks
207    pub fn has_mixed_timelocks(&self) -> bool {
208        self.ext.timelock_info.contains_unspendable_path()
209    }
210
211    /// Whether the miniscript has repeated Pk or Pkh
212    pub fn has_repeated_keys(&self) -> bool {
213        // Simple way to check whether all of these are correct is
214        // to have an iterator
215        let all_pkhs_len = self.iter_pk().count();
216
217        let unique_pkhs_len = self.iter_pk().collect::<HashSet<_>>().len();
218
219        unique_pkhs_len != all_pkhs_len
220    }
221
222    /// Whether the given miniscript contains a raw pkh fragment
223    pub fn contains_raw_pkh(&self) -> bool {
224        self.iter().any(|ms| match ms.node {
225            Terminal::RawPkH(_) => true,
226            _ => false,
227        })
228    }
229
230    /// Check whether the underlying Miniscript is safe under the current context
231    /// Lifting these polices would create a semantic representation that does
232    /// not represent the underlying semantics when miniscript is spent.
233    /// Signing logic may not find satisfaction even if one exists.
234    ///
235    /// For most cases, users should be dealing with safe scripts.
236    /// Use this function to check whether the guarantees of library hold.
237    /// Most functions of the library like would still
238    /// work, but results cannot be relied upon
239    pub fn sanity_check(&self) -> Result<(), AnalysisError> {
240        if !self.requires_sig() {
241            Err(AnalysisError::SiglessBranch)
242        } else if !self.is_non_malleable() {
243            Err(AnalysisError::Malleable)
244        } else if !self.within_resource_limits() {
245            Err(AnalysisError::BranchExceedResouceLimits)
246        } else if self.has_repeated_keys() {
247            Err(AnalysisError::RepeatedPubkeys)
248        } else if self.has_mixed_timelocks() {
249            Err(AnalysisError::HeightTimelockCombination)
250        } else {
251            Ok(())
252        }
253    }
254
255    /// Check whether the miniscript follows the given Extra policy [`ExtParams`]
256    pub fn ext_check(&self, ext: &ExtParams) -> Result<(), AnalysisError> {
257        if !ext.top_unsafe && !self.requires_sig() {
258            Err(AnalysisError::SiglessBranch)
259        } else if !ext.malleability && !self.is_non_malleable() {
260            Err(AnalysisError::Malleable)
261        } else if !ext.resource_limitations && !self.within_resource_limits() {
262            Err(AnalysisError::BranchExceedResouceLimits)
263        } else if !ext.repeated_pk && self.has_repeated_keys() {
264            Err(AnalysisError::RepeatedPubkeys)
265        } else if !ext.timelock_mixing && self.has_mixed_timelocks() {
266            Err(AnalysisError::HeightTimelockCombination)
267        } else if !ext.raw_pkh && self.contains_raw_pkh() {
268            Err(AnalysisError::ContainsRawPkh)
269        } else {
270            Ok(())
271        }
272    }
273}