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