bsv/script/
script_template.rs

1use crate::OpCodes::OP_0;
2use std::str::FromStr;
3
4use crate::{BSVErrors, OpCodes, PublicKey, Script, ScriptBit, Signature, VarInt};
5use hex::FromHexError;
6use num_traits::FromPrimitive;
7use serde::{Deserialize, Serialize};
8use strum_macros::Display;
9use thiserror::Error;
10
11#[derive(Debug, Error)]
12pub enum ScriptTemplateErrors {
13    #[error("Script did not match template at index {0}. {2} is not equal to {1:?}. Error: {3:?}")]
14    MatchFailure(usize, MatchToken, ScriptBit, BSVErrors),
15
16    #[error("Failed to parse OP_DATA code {0}: {1}")]
17    OpDataParse(String, String),
18
19    #[error("Script Template and Script lengths do not match.")]
20    LengthsDiffer,
21
22    #[error("{0}")]
23    MalformedHex(
24        #[from]
25        #[source]
26        FromHexError,
27    ),
28}
29
30#[derive(Debug, Clone, Display)]
31pub enum DataLengthConstraints {
32    Equals,
33    GreaterThan,
34    LessThan,
35    GreaterThanOrEquals,
36    LessThanOrEquals,
37}
38
39#[derive(Debug, Clone, Display)]
40pub enum MatchToken {
41    // Precise Matches
42    OpCode(OpCodes),
43    Push(Vec<u8>),
44    PushData(OpCodes, Vec<u8>),
45
46    // Fuzzy matches
47    AnyData,
48    Data(usize, DataLengthConstraints),
49    Signature,
50    PublicKey,
51    PublicKeyHash,
52}
53
54#[derive(Debug, Clone, Display, Serialize, Deserialize)]
55pub enum MatchDataTypes {
56    Data,
57    Signature,
58    PublicKey,
59    PublicKeyHash,
60}
61
62#[derive(Debug, Clone)]
63pub struct ScriptTemplate(Vec<MatchToken>);
64
65impl ScriptTemplate {
66    fn map_string_to_match_token(code: &str) -> Result<MatchToken, ScriptTemplateErrors> {
67        // Number OP_CODES
68        if code.len() < 3 {
69            if let Ok(num_code) = u8::from_str(code) {
70                match num_code {
71                    0 => return Ok(MatchToken::OpCode(OP_0)),
72                    v @ 1..=16 => return Ok(MatchToken::OpCode(OpCodes::from_u8(v + 80).unwrap())),
73                    _ => (),
74                }
75            }
76        }
77
78        // Standard OP_CODES
79        match OpCodes::from_str(code) {
80            Ok(OpCodes::OP_SIG) => return Ok(MatchToken::Signature),
81            Ok(OpCodes::OP_PUBKEY) => return Ok(MatchToken::PublicKey),
82            Ok(OpCodes::OP_PUBKEYHASH) => return Ok(MatchToken::PublicKeyHash),
83            Ok(OpCodes::OP_DATA) => return Ok(MatchToken::AnyData),
84
85            Ok(v) => return Ok(MatchToken::OpCode(v)),
86            Err(_) => (),
87        }
88
89        if code.starts_with(&OpCodes::OP_DATA.to_string()) {
90            // Match on >=
91            if let Some((_, length_str)) = code.split_once(">=") {
92                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
93                return Ok(MatchToken::Data(len, DataLengthConstraints::GreaterThanOrEquals));
94            }
95
96            // Match on <=
97            if let Some((_, length_str)) = code.split_once("<=") {
98                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
99                return Ok(MatchToken::Data(len, DataLengthConstraints::LessThanOrEquals));
100            }
101
102            // Match on =
103            if let Some((_, length_str)) = code.split_once('=') {
104                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
105                return Ok(MatchToken::Data(len, DataLengthConstraints::Equals));
106            }
107
108            // Match on >
109            if let Some((_, length_str)) = code.split_once('>') {
110                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
111                return Ok(MatchToken::Data(len, DataLengthConstraints::GreaterThan));
112            }
113
114            // Match on <
115            if let Some((_, length_str)) = code.split_once('<') {
116                let len = usize::from_str(length_str).map_err(|e| ScriptTemplateErrors::OpDataParse(code.to_string(), e.to_string()))?;
117                return Ok(MatchToken::Data(len, DataLengthConstraints::LessThan));
118            }
119        }
120
121        // PUSHDATA OP_CODES
122        let data_bytes = hex::decode(code)?;
123        let token = match VarInt::get_pushdata_opcode(data_bytes.len() as u64) {
124            Some(v) => MatchToken::PushData(v, data_bytes),
125            None => MatchToken::Push(data_bytes),
126        };
127
128        Ok(token)
129    }
130
131    pub fn from_script_impl(script: &Script) -> Result<ScriptTemplate, ScriptTemplateErrors> {
132        ScriptTemplate::from_asm_string_impl(&script.to_asm_string_impl(false))
133    }
134
135    pub fn from_asm_string_impl(asm: &str) -> Result<ScriptTemplate, ScriptTemplateErrors> {
136        let tokens: Result<Vec<_>, _> = asm.split(' ').map(ScriptTemplate::map_string_to_match_token).collect();
137
138        Ok(ScriptTemplate(tokens?))
139    }
140}
141
142impl ScriptTemplate {
143    pub fn from_script(script: &Script) -> Result<ScriptTemplate, ScriptTemplateErrors> {
144        ScriptTemplate::from_script_impl(script)
145    }
146
147    pub fn from_asm_string(asm: &str) -> Result<ScriptTemplate, ScriptTemplateErrors> {
148        ScriptTemplate::from_asm_string_impl(asm)
149    }
150}
151
152// #[cfg(all(feature = "wasm-bindgen-script-template"))]
153// #[cfg_attr(all(feature = "wasm-bindgen-script-template"), wasm_bindgen)]
154// impl ScriptTemplate {
155//     pub fn from_script(script: &Script) -> Result<ScriptTemplate, wasm_bindgen::JsError> {
156//         Ok(ScriptTemplate::from_script_impl(script)?)
157//     }
158
159//     pub fn from_asm_string(asm: &str) -> Result<ScriptTemplate, wasm_bindgen::JsError> {
160//         Ok(ScriptTemplate::from_asm_string_impl(asm)?)
161//     }
162// }
163
164/**
165 * Script Template
166 */
167impl Script {
168    pub fn match_impl(&self, script_template: &ScriptTemplate) -> Result<Vec<(MatchDataTypes, Vec<u8>)>, ScriptTemplateErrors> {
169        if self.0.len() != script_template.0.len() {
170            return Err(ScriptTemplateErrors::LengthsDiffer);
171        }
172
173        let mut matches = vec![];
174
175        for (i, (template, script)) in script_template.0.iter().zip(self.0.iter()).enumerate() {
176            let is_match = match (template, script) {
177                (MatchToken::OpCode(tmpl_code), ScriptBit::OpCode(op_code)) => Ok(tmpl_code == op_code),
178                (MatchToken::Push(tmpl_data), ScriptBit::Push(data)) => Ok(*tmpl_data == *data),
179                (MatchToken::PushData(tmpl_op, tmpl_data), ScriptBit::PushData(op, data)) => Ok(tmpl_op == op && tmpl_data == data),
180
181                (MatchToken::Data(len, constraint), ScriptBit::PushData(_, data) | ScriptBit::Push(data)) => match constraint {
182                    DataLengthConstraints::Equals => Ok(&data.len() == len),
183                    DataLengthConstraints::GreaterThan => Ok(&data.len() > len),
184                    DataLengthConstraints::LessThan => Ok(&data.len() < len),
185                    DataLengthConstraints::GreaterThanOrEquals => Ok(&data.len() >= len),
186                    DataLengthConstraints::LessThanOrEquals => Ok(&data.len() <= len),
187                },
188
189                (MatchToken::AnyData, ScriptBit::Push(_)) => Ok(true),
190                (MatchToken::AnyData, ScriptBit::PushData(_, _)) => Ok(true),
191
192                (MatchToken::Signature, ScriptBit::Push(sig_buf)) => Signature::from_der_impl(sig_buf).map(|_| true),
193
194                (MatchToken::PublicKey, ScriptBit::Push(pubkey_buf)) => PublicKey::from_bytes_impl(pubkey_buf).map(|_| true),
195
196                (MatchToken::PublicKeyHash, ScriptBit::Push(pubkeyhash_buf)) => Ok(pubkeyhash_buf.len() == 20), // OP_HASH160
197
198                _ => Ok(false),
199            };
200
201            match is_match {
202                Ok(true) => (),
203                Ok(false) => {
204                    return Err(ScriptTemplateErrors::MatchFailure(
205                        i,
206                        template.clone(),
207                        script.clone(),
208                        BSVErrors::GenericError(format!("{} != {}", template, script)),
209                    ));
210                }
211                Err(e) => {
212                    return Err(ScriptTemplateErrors::MatchFailure(i, template.clone(), script.clone(), e));
213                }
214            }
215
216            // Now that we know script bit is a match, we can add the data parts to the matches array.
217            match (template, script) {
218                (MatchToken::Data(_, _), ScriptBit::PushData(_, data) | ScriptBit::Push(data)) => matches.push((MatchDataTypes::Data, data.clone())),
219
220                (MatchToken::AnyData, ScriptBit::Push(data)) => matches.push((MatchDataTypes::Data, data.clone())),
221                (MatchToken::AnyData, ScriptBit::PushData(_, data)) => matches.push((MatchDataTypes::Data, data.clone())),
222
223                (MatchToken::Signature, ScriptBit::Push(data)) => matches.push((MatchDataTypes::Signature, data.clone())),
224
225                (MatchToken::PublicKey, ScriptBit::Push(data)) => matches.push((MatchDataTypes::PublicKey, data.clone())),
226
227                (MatchToken::PublicKeyHash, ScriptBit::Push(data)) => matches.push((MatchDataTypes::PublicKeyHash, data.clone())), // OP_HASH160
228                _ => (),
229            }
230        }
231
232        Ok(matches)
233    }
234
235    pub fn test_impl(&self, script_template: &ScriptTemplate) -> bool {
236        self.match_impl(script_template).is_ok()
237    }
238}
239
240impl Script {
241    /// Matches the Script against the provided ScriptTemplate.
242    ///
243    /// If any data can be gleaned from the Script (ie. OP_DATA, OP_PUBKEY, OP_SIG, etc.), it will return it in a `Vec<Match>`
244    ///
245    /// # Example
246    /// ```
247    /// use bsv::{ Script, MatchDataTypes, ScriptTemplate };
248    ///
249    /// let script = Script::from_asm_string("OP_HASH160 b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc6 OP_EQUALVERIFY OP_DUP OP_HASH160 f9dfc5a4ae5256e5938c2d819738f7b57e4d7b46 OP_EQUALVERIFY OP_CHECKSIG OP_RETURN 21e8").unwrap();
250    /// let script_template = ScriptTemplate::from_asm_string("OP_HASH160 OP_DATA=20 OP_EQUALVERIFY OP_DUP OP_HASH160 OP_PUBKEYHASH OP_EQUALVERIFY OP_CHECKSIG OP_RETURN OP_DATA").unwrap();
251    ///
252    /// let match_result = script.matches(&script_template);
253    /// let extracted = match_result.unwrap();
254    /// assert_eq!(extracted.len(), 3);
255    /// match &extracted[0] {
256    ///    (MatchDataTypes::Data, v) => {
257    ///        assert_eq!(v.len(), 20, "Data was not 20 bytes long");
258    ///        assert_eq!(v, &hex::decode("b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc6").unwrap())
259    ///    }
260    ///    _ => assert!(false, "Index 0 did not contain Signature"),
261    /// }
262    /// ```
263    pub fn matches(&self, script_template: &ScriptTemplate) -> Result<Vec<(MatchDataTypes, Vec<u8>)>, ScriptTemplateErrors> {
264        self.match_impl(script_template)
265    }
266
267    /// Matches the Script against the provided ScriptTemplate.
268    ///
269    /// Returns `true` if the Script matches the ScriptTemplate.
270    pub fn is_match(&self, script_template: &ScriptTemplate) -> bool {
271        self.test_impl(script_template)
272    }
273}
274
275//#[cfg(all(feature = "wasm-bindgen-script-template"))]
276//#[cfg_attr(all(feature = "wasm-bindgen-script-template"), wasm_bindgen)]
277//impl Script {
278//    /// Matches the Script against the provided ScriptTemplate.
279//    ///
280//    /// If any data can be gleaned from the Script (ie. OP_DATA, OP_PUBKEY, OP_SIG, etc.), it will return it in a `Vec<Match>`
281//    /// @returns {[string, Uint8Array][]}
282//    pub fn matches(&self, script_template: &ScriptTemplate) -> Result<JsValue, wasm_bindgen::JsError> {
283//        let matches = self.match_impl(script_template)?;
284
285//        Ok(serde_wasm_bindgen::to_value(&matches)?)
286//    }
287
288//    /// Matches the Script against the provided ScriptTemplate.
289//    ///
290//    /// Returns `true` if the Script matches the ScriptTemplate.
291//    #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = isMatch))]
292//    pub fn is_match(&self, script_template: &ScriptTemplate) -> bool {
293//        self.test_impl(script_template)
294//    }
295//}