Skip to main content

elements_miniscript/extensions/
outputs_pref.rs

1//! Miniscript extension: outputs_pref
2//! Note that this fragment is only supported for Segwit context
3//! You are most likely looking for taproot direct tx introspection
4
5use std::fmt;
6
7use bitcoin::hashes::{sha256d, Hash};
8use bitcoin::hex::{DisplayHex, FromHex};
9use elements::encode::serialize;
10
11use super::{FromTokenIterError, ParseableExt, TxEnv};
12use crate::descriptor::CovError;
13use crate::miniscript::astelem::StackCtxOperations;
14use crate::miniscript::context::ScriptContextError;
15use crate::miniscript::lex::{Token as Tk, TokenIter};
16use crate::miniscript::limits::{MAX_SCRIPT_ELEMENT_SIZE, MAX_STANDARD_P2WSH_STACK_ITEM_SIZE};
17use crate::miniscript::satisfy::{Satisfaction, Witness};
18use crate::miniscript::types::extra_props::{OpLimits, TimelockInfo};
19use crate::miniscript::types::{Base, Correctness, Dissat, ExtData, Input, Malleability};
20use crate::policy::{self, Liftable};
21use crate::{expression, interpreter, Error, Extension, MiniscriptKey, Satisfier, ToPublicKey};
22
23/// Prefix is initally encoded in the script pubkey
24/// User provides a suffix such that hash of (prefix || suffix)
25/// is equal to hashOutputs
26/// Since, there is a policy restriction that initial pushes must be
27/// only 80 bytes, we need user to provide suffix in separate items
28/// There can be atmost 7 cats, because the script element must be less
29/// than 520 bytes total in order to compute an hash256 on it.
30/// Even if the witness does not require 7 pushes, the user should push
31/// 7 elements with possibly empty values.
32///
33/// `CAT CAT CAT CAT CAT CAT <pref> SWAP CAT` /*Now we hashoutputs on stack */
34/// `HASH256`
35/// `DEPTH <10> SUB PICK EQUALVERIFY`
36#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone)]
37pub struct LegacyOutputsPref {
38    /// the version of transaction
39    pub pref: Vec<u8>,
40}
41
42impl fmt::Display for LegacyOutputsPref {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "outputs_pref({})", self.pref.as_hex())
45    }
46}
47
48impl<Pk: MiniscriptKey> Liftable<Pk> for LegacyOutputsPref {
49    fn lift(&self) -> Result<policy::Semantic<Pk>, Error> {
50        Err(Error::CovError(CovError::CovenantLift))
51    }
52}
53
54impl Extension for LegacyOutputsPref {
55    fn segwit_ctx_checks(&self) -> Result<(), ScriptContextError> {
56        if self.pref.len() > MAX_SCRIPT_ELEMENT_SIZE {
57            Err(ScriptContextError::CovElementSizeExceeded)
58        } else {
59            Ok(())
60        }
61    }
62
63    fn corr_prop(&self) -> Correctness {
64        Correctness {
65            base: Base::B,
66            input: Input::Any,    // 7 outputs
67            dissatisfiable: true, // Any 7 elements that don't cat
68            unit: true,
69        }
70    }
71
72    fn mall_prop(&self) -> Malleability {
73        Malleability {
74            dissat: Dissat::Unknown,
75            safe: false,
76            non_malleable: true,
77        }
78    }
79
80    fn extra_prop(&self) -> ExtData {
81        // Assume txouts fill out all the 520 bytes
82        let max_wit_sz = MAX_SCRIPT_ELEMENT_SIZE - self.pref.len();
83        ExtData {
84            pk_cost: 8 + self.pref.len() + 1 + 6, // See script_size() in astelem.rs
85            has_free_verify: true,
86            stack_elem_count_sat: Some(7),
87            stack_elem_count_dissat: Some(7),
88            max_sat_size: Some((max_wit_sz, max_wit_sz)),
89            max_dissat_size: Some((0, 0)), // all empty should dissatisfy
90            timelock_info: TimelockInfo::default(),
91            exec_stack_elem_count_sat: Some(3), // sha2 context, byte slice, target hash
92            exec_stack_elem_count_dissat: Some(3),
93            ops: OpLimits {
94                count: 13,
95                sat: Some(0),
96                nsat: Some(0),
97            },
98        }
99    }
100
101    fn script_size(&self) -> usize {
102        // CAT CAT CAT CAT CAT CAT <pref> SWAP CAT /*Now we hashoutputs on stack */
103        // HASH256 DEPTH <10> SUB PICK EQUAL
104        8 + self.pref.len() + 1 /* line1 opcodes + pref.push */
105                + 6 /* line 2 */
106    }
107
108    fn from_name_tree(
109        name: &str,
110        children: &[expression::Tree<'_>],
111    ) -> Result<Self, FromTokenIterError> {
112        if children.len() == 1 && name == "outputs_pref" {
113            let pref = expression::terminal(&children[0], Vec::<u8>::from_hex)
114                .map_err(|_| FromTokenIterError)?;
115            Ok(Self { pref })
116        } else {
117            // Correct error handling while parsing fromtree
118            Err(FromTokenIterError)
119        }
120    }
121}
122
123impl ParseableExt for LegacyOutputsPref {
124    fn satisfy<Pk, S>(&self, sat: &S) -> Satisfaction
125    where
126        Pk: ToPublicKey,
127        S: Satisfier<Pk>,
128    {
129        let wit = match sat.lookup_outputs() {
130            Some(outs) => {
131                let mut ser_out = Vec::new();
132                let num_wit_elems =
133                    MAX_SCRIPT_ELEMENT_SIZE / MAX_STANDARD_P2WSH_STACK_ITEM_SIZE + 1;
134                let mut witness = Vec::with_capacity(num_wit_elems);
135                for out in outs {
136                    ser_out.extend(serialize(out));
137                }
138                // We need less than 520 bytes of serialized hashoutputs
139                // in order to compute hash256 inside script
140                if ser_out.len() > MAX_SCRIPT_ELEMENT_SIZE {
141                    Witness::Impossible
142                } else if ser_out.starts_with(&self.pref) {
143                    let mut iter = ser_out.into_iter().skip(self.pref.len()).peekable();
144
145                    while iter.peek().is_some() {
146                        let chk_size = MAX_STANDARD_P2WSH_STACK_ITEM_SIZE;
147                        let chunk: Vec<u8> = iter.by_ref().take(chk_size).collect();
148                        witness.push(chunk);
149                    }
150                    // Append empty elems to make for extra cats
151                    // in the spk
152                    while witness.len() < num_wit_elems {
153                        witness.push(vec![]);
154                    }
155                    Witness::Stack(witness)
156                } else {
157                    Witness::Impossible
158                }
159            }
160            // Note the unavailable instead of impossible because we don't know
161            // the hashoutputs yet
162            None => Witness::Unavailable,
163        };
164        Satisfaction {
165            stack: wit,
166            has_sig: false,
167        }
168    }
169
170    fn dissatisfy<Pk, S>(&self, sat: &S) -> Satisfaction
171    where
172        Pk: ToPublicKey,
173        S: Satisfier<Pk>,
174    {
175        let wit = match sat.lookup_outputs() {
176            Some(outs) => {
177                let mut ser_out = Vec::new();
178                for out in outs {
179                    ser_out.extend(serialize(out));
180                }
181                let num_wit_elems = MAX_SCRIPT_ELEMENT_SIZE / MAX_STANDARD_P2WSH_STACK_ITEM_SIZE;
182                let mut witness = Vec::with_capacity(num_wit_elems);
183                if self.pref != ser_out.as_slice() {
184                    while witness.len() < num_wit_elems {
185                        witness.push(vec![]);
186                    }
187                    Witness::Stack(witness)
188                } else if self.pref.len() != MAX_SCRIPT_ELEMENT_SIZE {
189                    // Case when prefix == ser_out and it is possible
190                    // to add more witness
191                    witness.push(vec![1]);
192                    while witness.len() < num_wit_elems {
193                        witness.push(vec![]);
194                    }
195                    Witness::Stack(witness)
196                } else {
197                    // case when pref == ser_out and len of both is 520
198                    Witness::Impossible
199                }
200            }
201            // Note the unavailable instead of impossible because we don't know
202            // the hashoutputs yet
203            None => Witness::Unavailable,
204        };
205        Satisfaction {
206            stack: wit,
207            has_sig: false,
208        }
209    }
210
211    fn push_to_builder(&self, builder: elements::script::Builder) -> elements::script::Builder {
212        builder.check_item_pref(4, &self.pref)
213    }
214
215    fn from_token_iter(tokens: &mut TokenIter<'_>) -> Result<Self, FromTokenIterError> {
216        let outputs_pref = {
217            let sl = tokens.peek_slice(15).ok_or(FromTokenIterError)?;
218            if let Tk::Push(pref) = &sl[6] {
219                if sl[0] == Tk::Cat
220                    && sl[1] == Tk::Cat
221                    && sl[2] == Tk::Cat
222                    && sl[3] == Tk::Cat
223                    && sl[4] == Tk::Cat
224                    && sl[5] == Tk::Cat
225                    && sl[7] == Tk::Swap
226                    && sl[8] == Tk::Cat
227                    && sl[9] == Tk::Hash256
228                    && sl[11] == Tk::Num(4)
229                    && sl[12] == Tk::Sub
230                    && sl[13] == Tk::Pick
231                    && sl[14] == Tk::Equal
232                {
233                    Self { pref: pref.clone() }
234                } else {
235                    return Err(FromTokenIterError);
236                }
237            } else {
238                return Err(FromTokenIterError);
239            }
240        };
241        tokens.advance(15).expect("Size checked previously");
242        Ok(outputs_pref)
243    }
244
245    fn evaluate(
246        &self,
247        stack: &mut interpreter::Stack,
248        _txenv: Option<&TxEnv>,
249    ) -> Result<bool, interpreter::Error> {
250        // Hash Outputs is at index 3
251        let hash_outputs = stack[3];
252        let hash_outputs = hash_outputs.try_push()?;
253        // Maximum number of suffix elements
254        let max_elems = MAX_SCRIPT_ELEMENT_SIZE / MAX_STANDARD_P2WSH_STACK_ITEM_SIZE + 1;
255        if hash_outputs.len() == 32 {
256            // We want to cat the last 6 elements(5 cats) in suffix
257            if stack.len() < max_elems {
258                return Err(interpreter::Error::UnexpectedStackEnd);
259            }
260            let mut outputs_builder = Vec::new();
261            outputs_builder.extend(&self.pref);
262            let len = stack.len();
263            // Add the max_elems suffix elements
264            for i in 0..max_elems {
265                outputs_builder.extend(stack[len - max_elems + i].into_slice());
266            }
267            // Pop the max_elems suffix elements
268            for _ in 0..max_elems {
269                stack.pop().unwrap();
270            }
271            if sha256d::Hash::hash(&outputs_builder).as_byte_array() == hash_outputs {
272                stack.push(interpreter::Element::Satisfied);
273                Ok(true)
274            } else {
275                Ok(false)
276            }
277        } else {
278            Err(interpreter::Error::CovWitnessSizeErr {
279                pos: 9,
280                expected: 32,
281                actual: hash_outputs.len(),
282            })
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use bitcoin::PublicKey;
290
291    use super::*;
292    use crate::{Miniscript, Segwitv0};
293
294    #[test]
295    fn test_outputs_pref() {
296        type MsExtVer = Miniscript<PublicKey, Segwitv0, LegacyOutputsPref>;
297
298        let ms = MsExtVer::from_str_insane("outputs_pref(aa)").unwrap();
299        // test string rtt
300        assert_eq!(ms.to_string(), "outputs_pref(aa)");
301        // script rtt
302        assert_eq!(ms, MsExtVer::parse_insane(&ms.encode()).unwrap())
303    }
304}