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}