Skip to main content

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