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}