1use crate::error::Result;
8use crate::opcodes::*;
9use crate::script::{is_op_success, is_push_opcode, op_advance};
10use crate::types::{ByteString, Transaction};
11
12pub const NULLDATA_CNTRPRTY: &[u8] = b"CNTRPRTY";
14pub const NULLDATA_OMNI: &[u8] = b"omni";
15pub const NULLDATA_OTS: &[u8] = &[0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01];
17
18const NULLDATA_MAGICS: &[&[u8]] = &[NULLDATA_CNTRPRTY, NULLDATA_OMNI, NULLDATA_OTS];
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum ScriptTemplate {
23 UnexecIf {
24 start: usize,
25 end: usize,
26 remainder: UnexecIfRemainder,
27 },
28 NullDataOp13,
29 NullDataMagic {
30 prefix: &'static [u8],
31 },
32 DataLikeMs {
33 n: u8,
34 m: u8,
35 data_like_keys: usize,
36 },
37 Unknown,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum UnexecIfRemainder {
42 TapChecksig { pubkey: [u8; 32] },
43}
44
45pub fn scan(script: &[u8]) -> ScriptTemplate {
47 if script.is_empty() {
48 return ScriptTemplate::Unknown;
49 }
50 if let Some(t) = scan_unexec_if(script) {
51 return t;
52 }
53 if let Some(t) = scan_nulldata(script) {
54 return t;
55 }
56 if let Some(t) = scan_data_like_ms(script) {
57 return t;
58 }
59 ScriptTemplate::Unknown
60}
61
62pub fn scan_witness<'a, I>(elements: I) -> ScriptTemplate
64where
65 I: IntoIterator<Item = &'a [u8]>,
66{
67 let mut first_other = ScriptTemplate::Unknown;
68 for el in elements {
69 match scan(el) {
70 u @ ScriptTemplate::UnexecIf { .. } => return u,
71 ScriptTemplate::Unknown => {}
72 other => {
73 if matches!(first_other, ScriptTemplate::Unknown) {
74 first_other = other;
75 }
76 }
77 }
78 }
79 first_other
80}
81
82pub fn scan_tx(tx: &Transaction, witnesses: Option<&[crate::witness::Witness]>) -> ScriptTemplate {
84 if let Some(wits) = witnesses {
85 for w in wits {
86 let hit = scan_witness(w.iter().map(|e| e.as_ref()));
87 if !matches!(hit, ScriptTemplate::Unknown) {
88 return hit;
89 }
90 }
91 }
92 for out in &tx.outputs {
93 let hit = scan(out.script_pubkey.as_ref());
94 if !matches!(hit, ScriptTemplate::Unknown) {
95 return hit;
96 }
97 }
98 ScriptTemplate::Unknown
99}
100
101fn has_op_success(script: &[u8]) -> bool {
102 let mut pc = 0usize;
103 while pc < script.len() {
104 if is_op_success(script[pc]) {
105 return true;
106 }
107 let adv = op_advance(script, pc);
108 if adv == 0 {
109 return true;
110 }
111 pc = pc.saturating_add(adv);
112 if pc > script.len() {
113 return true;
114 }
115 }
116 false
117}
118
119fn scan_unexec_if(script: &[u8]) -> Option<ScriptTemplate> {
120 if has_op_success(script) {
121 return None;
122 }
123 let (start, end) = find_single_unexec_if(script)?;
124 let remainder = match_tap_checksig_remainder(script, start, end)?;
125 Some(ScriptTemplate::UnexecIf {
126 start,
127 end,
128 remainder,
129 })
130}
131
132fn find_single_unexec_if(script: &[u8]) -> Option<(usize, usize)> {
134 let mut pc = 0usize;
135 let mut found: Option<(usize, usize)> = None;
136 while pc < script.len() {
137 if pc + 1 < script.len() && script[pc] == OP_0 && script[pc + 1] == OP_IF {
138 let mut inner = pc + 2;
139 loop {
140 if inner >= script.len() {
141 return None;
142 }
143 let op = script[inner];
144 if op == OP_ENDIF {
145 let end = inner + 1;
146 if found.is_some() {
147 return None;
148 }
149 found = Some((pc, end));
150 pc = end;
151 break;
152 }
153 if op == OP_IF || op == OP_NOTIF || op == OP_ELSE {
154 return None;
155 }
156 if !is_push_opcode(op) {
157 return None;
158 }
159 let adv = op_advance(script, inner);
160 if adv == 0 || inner + adv > script.len() {
161 return None;
162 }
163 inner += adv;
164 }
165 } else {
166 let adv = op_advance(script, pc);
167 if adv == 0 || pc + adv > script.len() {
168 return None;
169 }
170 pc += adv;
171 }
172 }
173 found
174}
175
176fn match_tap_checksig_remainder(
177 script: &[u8],
178 start: usize,
179 end: usize,
180) -> Option<UnexecIfRemainder> {
181 const LEAF_LEN: usize = 34;
182 if start == LEAF_LEN && end == script.len() {
183 return tap_checksig_leaf(&script[0..LEAF_LEN]);
184 }
185 if start == 0 && end + LEAF_LEN == script.len() {
186 return tap_checksig_leaf(&script[end..]);
187 }
188 None
189}
190
191fn tap_checksig_leaf(leaf: &[u8]) -> Option<UnexecIfRemainder> {
192 if leaf.len() != 34 || leaf[0] != PUSH_32_BYTES || leaf[33] != OP_CHECKSIG {
193 return None;
194 }
195 let mut pubkey = [0u8; 32];
196 pubkey.copy_from_slice(&leaf[1..33]);
197 Some(UnexecIfRemainder::TapChecksig { pubkey })
198}
199
200fn scan_nulldata(script: &[u8]) -> Option<ScriptTemplate> {
201 if script.is_empty() || script[0] != OP_RETURN {
202 return None;
203 }
204 if script.len() == 1 {
205 return None;
206 }
207 if script[1] == OP_13 {
209 let mut pc = 2usize;
210 while pc < script.len() {
211 if !is_push_opcode(script[pc]) {
212 return None;
213 }
214 let adv = op_advance(script, pc);
215 if adv == 0 || pc + adv > script.len() {
216 return None;
217 }
218 pc += adv;
219 }
220 return Some(ScriptTemplate::NullDataOp13);
221 }
222 let payload = push_payload_after_op_return(script)?;
223 for magic in NULLDATA_MAGICS {
224 if payload.starts_with(magic) {
225 return Some(ScriptTemplate::NullDataMagic { prefix: magic });
226 }
227 }
228 None
229}
230
231fn push_payload_after_op_return(script: &[u8]) -> Option<&[u8]> {
233 if script.len() < 2 {
234 return None;
235 }
236 let op = script[1];
237 match op {
238 0x01..=0x4b => {
239 let n = op as usize;
240 if script.len() < 2 + n {
241 return None;
242 }
243 Some(&script[2..2 + n])
244 }
245 0x4c => {
246 if script.len() < 3 {
247 return None;
248 }
249 let n = script[2] as usize;
250 if script.len() < 3 + n {
251 return None;
252 }
253 Some(&script[3..3 + n])
254 }
255 0x4d => {
256 if script.len() < 4 {
257 return None;
258 }
259 let n = u16::from_le_bytes([script[2], script[3]]) as usize;
260 if script.len() < 4 + n {
261 return None;
262 }
263 Some(&script[4..4 + n])
264 }
265 OP_1..=OP_16 | OP_0 => Some(&[]),
266 _ => None,
267 }
268}
269
270fn scan_data_like_ms(script: &[u8]) -> Option<ScriptTemplate> {
271 let (m, n, keys) = parse_bare_multisig_keys(script)?;
272 if keys.is_empty() {
273 return None;
274 }
275 let data_like = keys.iter().filter(|k| !looks_compressed_pubkey(k)).count();
276 if data_like != keys.len() {
277 return None;
278 }
279 Some(ScriptTemplate::DataLikeMs {
280 n,
281 m,
282 data_like_keys: data_like,
283 })
284}
285
286fn looks_compressed_pubkey(key: &[u8]) -> bool {
287 key.len() == 33 && (key[0] == 0x02 || key[0] == 0x03)
288}
289
290fn parse_bare_multisig_keys(script: &[u8]) -> Option<(u8, u8, Vec<&[u8]>)> {
291 if script.len() < 4 || script[script.len() - 1] != OP_CHECKMULTISIG {
292 return None;
293 }
294 let n_op = script[script.len() - 2];
295 if !(OP_1..=OP_16).contains(&n_op) {
296 return None;
297 }
298 let n = n_op - OP_1 + 1;
299 let m_op = script[0];
300 if !(OP_1..=OP_16).contains(&m_op) {
301 return None;
302 }
303 let m = m_op - OP_1 + 1;
304 if m == 0 || m > n {
305 return None;
306 }
307 let mut i = 1usize;
308 let mut keys = Vec::with_capacity(n as usize);
309 let trailer = script.len() - 2;
310 for _ in 0..n {
311 if i >= trailer {
312 return None;
313 }
314 let first = script[i];
315 let (start, len) = if (0x01..=0x4b).contains(&first) {
316 (i + 1, first as usize)
317 } else {
318 return None;
319 };
320 if start + len > trailer {
321 return None;
322 }
323 keys.push(&script[start..start + len]);
324 i = start + len;
325 }
326 if i != trailer {
327 return None;
328 }
329 Some((m, n, keys))
330}
331
332#[cfg(feature = "production")]
334#[allow(clippy::too_many_arguments)]
335pub fn try_verify_p2tr_unexec_if_fast_path(
336 script_sig: &ByteString,
337 script_pubkey: &[u8],
338 witness: &crate::witness::Witness,
339 _flags: u32,
340 tx: &Transaction,
341 input_index: usize,
342 prevout_values: &[i64],
343 prevout_script_pubkeys: &[&[u8]],
344 block_height: Option<u64>,
345 network: crate::types::Network,
346 schnorr_collector: Option<&crate::bip348::SchnorrSignatureCollector>,
347) -> Option<Result<bool>> {
348 use crate::activation::taproot_active_at_height;
349 use crate::taproot::parse_taproot_script_path_witness;
350
351 if !taproot_active_at_height(block_height, network) {
352 return None;
353 }
354 if script_pubkey.len() != 34 || script_pubkey[0] != OP_1 || script_pubkey[1] != PUSH_32_BYTES {
355 return None;
356 }
357 if !script_sig.is_empty() {
358 return None;
359 }
360 if witness.len() < 2 {
361 return None;
362 }
363 let mut output_key = [0u8; 32];
364 output_key.copy_from_slice(&script_pubkey[2..34]);
365 let (witness_body, annex_hash) = crate::taproot::strip_taproot_annex(witness);
366 let parsed = match parse_taproot_script_path_witness(&witness_body, &output_key) {
367 Ok(Some(p)) => p,
368 Ok(None) | Err(_) => return None,
369 };
370 let (tapscript, stack_items, control_block) = parsed;
371 if control_block.leaf_version != crate::taproot::TAPROOT_LEAF_VERSION_TAPSCRIPT {
372 return None;
373 }
374 if stack_items.len() != 1 {
375 return None;
376 }
377 let ScriptTemplate::UnexecIf {
378 remainder: UnexecIfRemainder::TapChecksig { pubkey },
379 ..
380 } = scan(tapscript.as_ref())
381 else {
382 return None;
383 };
384 let (sig_bytes, sighash_type) =
385 crate::bip348::try_parse_taproot_schnorr_witness_sig(stack_items[0].as_ref())?;
386 let sighash = crate::taproot::compute_tapscript_signature_hash(
387 tx,
388 input_index,
389 prevout_values,
390 prevout_script_pubkeys,
391 tapscript.as_ref(),
392 control_block.leaf_version,
393 0xffff_ffff,
394 sighash_type,
395 annex_hash.as_ref(),
396 )
397 .ok()?;
398 let result = crate::bip348::verify_tapscript_schnorr_signature(
399 &sighash,
400 &pubkey,
401 &sig_bytes,
402 schnorr_collector,
403 );
404 Some(result)
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 fn push32_checksig(pk: [u8; 32]) -> Vec<u8> {
412 let mut s = vec![PUSH_32_BYTES];
413 s.extend_from_slice(&pk);
414 s.push(OP_CHECKSIG);
415 s
416 }
417
418 fn envelope(body: &[u8]) -> Vec<u8> {
419 let mut s = vec![OP_0, OP_IF];
420 s.extend_from_slice(body);
421 s.push(OP_ENDIF);
422 s
423 }
424
425 #[test]
426 fn unexec_if_suffix_checksig() {
427 let pk = [0x11u8; 32];
428 let mut script = envelope(&[0x03, b'o', b'r', b'd']);
429 script.extend(push32_checksig(pk));
430 match scan(&script) {
431 ScriptTemplate::UnexecIf {
432 remainder: UnexecIfRemainder::TapChecksig { pubkey },
433 ..
434 } => assert_eq!(pubkey, pk),
435 other => panic!("expected UnexecIf, got {other:?}"),
436 }
437 }
438
439 #[test]
440 fn unexec_if_prefix_checksig() {
441 let pk = [0x22u8; 32];
442 let mut script = push32_checksig(pk);
443 script.extend(envelope(&[0x03, b'o', b'r', b'd']));
444 match scan(&script) {
445 ScriptTemplate::UnexecIf {
446 remainder: UnexecIfRemainder::TapChecksig { pubkey },
447 ..
448 } => assert_eq!(pubkey, pk),
449 other => panic!("expected UnexecIf, got {other:?}"),
450 }
451 }
452
453 #[test]
454 fn two_envelopes_unknown() {
455 let mut script = envelope(&[0x01, 0xaa]);
456 script.extend(envelope(&[0x01, 0xbb]));
457 script.extend(push32_checksig([0x33; 32]));
458 assert_eq!(scan(&script), ScriptTemplate::Unknown);
459 }
460
461 #[test]
462 fn nested_if_unknown() {
463 let mut script = vec![OP_0, OP_IF, OP_0, OP_IF, OP_ENDIF, OP_ENDIF];
464 script.extend(push32_checksig([0x44; 32]));
465 assert_eq!(scan(&script), ScriptTemplate::Unknown);
466 }
467
468 #[test]
469 fn leftover_opcode_unknown() {
470 let mut script = envelope(&[0x01, 0xaa]);
471 script.push(OP_DUP);
472 script.extend(push32_checksig([0x55; 32]));
473 assert_eq!(scan(&script), ScriptTemplate::Unknown);
474 }
475
476 #[test]
477 fn leftover_after_checksig_unknown() {
478 let mut script = envelope(&[0x01, 0xaa]);
479 script.extend(push32_checksig([0x55; 32]));
480 script.push(OP_DROP);
481 assert_eq!(scan(&script), ScriptTemplate::Unknown);
482 }
483
484 #[test]
485 fn op_success_unknown() {
486 let mut script = vec![OP_0, OP_IF, 0xba, OP_ENDIF];
488 script.extend(push32_checksig([0x66; 32]));
489 assert_eq!(scan(&script), ScriptTemplate::Unknown);
490 }
491
492 #[test]
493 fn nulldata_op13() {
494 let script = vec![OP_RETURN, OP_13, 0x01, 0xff];
495 assert_eq!(scan(&script), ScriptTemplate::NullDataOp13);
496 }
497
498 #[test]
499 fn nulldata_cntrprty() {
500 let mut script = vec![OP_RETURN, NULLDATA_CNTRPRTY.len() as u8];
501 script.extend_from_slice(NULLDATA_CNTRPRTY);
502 match scan(&script) {
503 ScriptTemplate::NullDataMagic { prefix } => assert_eq!(prefix, NULLDATA_CNTRPRTY),
504 other => panic!("expected NullDataMagic, got {other:?}"),
505 }
506 }
507
508 #[test]
509 fn data_like_ms_all_invalid_prefix() {
510 let mut script = vec![OP_1, 0x08];
512 script.extend_from_slice(&[0x11; 8]);
513 script.push(0x08);
514 script.extend_from_slice(&[0x22; 8]);
515 script.push(OP_2);
516 script.push(OP_CHECKMULTISIG);
517 match scan(&script) {
518 ScriptTemplate::DataLikeMs {
519 n,
520 m,
521 data_like_keys,
522 } => {
523 assert_eq!((m, n, data_like_keys), (1, 2, 2));
524 }
525 other => panic!("expected DataLikeMs, got {other:?}"),
526 }
527 }
528
529 #[test]
530 fn real_compressed_key_not_data_like() {
531 let mut script = vec![OP_1, PUSH_33_BYTES, 0x02];
532 script.extend_from_slice(&[0xab; 32]);
533 script.push(OP_1);
534 script.push(OP_CHECKMULTISIG);
535 assert_eq!(scan(&script), ScriptTemplate::Unknown);
536 }
537}