Skip to main content

bsv_rs/script/
script.rs

1//! Bitcoin Script class.
2//!
3//! The Script class represents a script in a Bitcoin SV transaction,
4//! encapsulating the functionality to construct, parse, and serialize
5//! scripts used in both locking (output) and unlocking (input) scripts.
6
7use std::cell::RefCell;
8
9use super::chunk::ScriptChunk;
10use super::op::*;
11use crate::primitives::{from_hex, to_hex};
12use crate::Result;
13
14/// The Script class represents a script in a Bitcoin SV transaction.
15///
16/// Scripts can be constructed from ASM strings, hex strings, or binary data,
17/// and can be serialized back to any of these formats.
18#[derive(Debug, Clone)]
19pub struct Script {
20    /// The chunks that make up this script
21    chunks: RefCell<Vec<ScriptChunk>>,
22    /// Whether the chunks have been parsed from raw bytes
23    parsed: RefCell<bool>,
24    /// Cached raw bytes (for lazy parsing)
25    raw_bytes_cache: RefCell<Option<Vec<u8>>>,
26    /// Cached hex string
27    hex_cache: RefCell<Option<String>>,
28}
29
30impl Script {
31    /// Creates a new empty script.
32    pub fn new() -> Self {
33        Self {
34            chunks: RefCell::new(Vec::new()),
35            parsed: RefCell::new(true),
36            raw_bytes_cache: RefCell::new(None),
37            hex_cache: RefCell::new(None),
38        }
39    }
40
41    /// Creates a script from a vector of chunks.
42    pub fn from_chunks(chunks: Vec<ScriptChunk>) -> Self {
43        Self {
44            chunks: RefCell::new(chunks),
45            parsed: RefCell::new(true),
46            raw_bytes_cache: RefCell::new(None),
47            hex_cache: RefCell::new(None),
48        }
49    }
50
51    /// Constructs a Script instance from an ASM (Assembly) formatted string.
52    ///
53    /// # Example
54    ///
55    /// ```
56    /// use bsv_rs::script::Script;
57    ///
58    /// let script = Script::from_asm("OP_DUP OP_HASH160 abcd1234 OP_EQUALVERIFY OP_CHECKSIG").unwrap();
59    /// ```
60    pub fn from_asm(asm: &str) -> Result<Self> {
61        let mut chunks = Vec::new();
62
63        if asm.is_empty() {
64            return Ok(Self::from_chunks(chunks));
65        }
66
67        let tokens: Vec<&str> = asm.split(' ').filter(|s| !s.is_empty()).collect();
68        let mut i = 0;
69
70        while i < tokens.len() {
71            let token = tokens[i];
72
73            // Check if it's an opcode
74            let op_code_num = name_to_opcode(token);
75
76            // Handle special cases: "0" and "-1"
77            if token == "0" {
78                chunks.push(ScriptChunk::new_opcode(OP_0));
79                i += 1;
80            } else if token == "-1" {
81                chunks.push(ScriptChunk::new_opcode(OP_1NEGATE));
82                i += 1;
83            } else if op_code_num.is_none() {
84                // It's hex data
85                let mut hex = token.to_string();
86                #[allow(unknown_lints, clippy::manual_is_multiple_of)]
87                if hex.len() % 2 != 0 {
88                    hex = format!("0{}", hex);
89                }
90
91                let data = from_hex(&hex)?;
92                let len = data.len();
93
94                let op = if len < OP_PUSHDATA1 as usize {
95                    len as u8
96                } else if len < 256 {
97                    OP_PUSHDATA1
98                } else if len < 65536 {
99                    OP_PUSHDATA2
100                } else {
101                    OP_PUSHDATA4
102                };
103
104                chunks.push(ScriptChunk::new(op, Some(data)));
105                i += 1;
106            } else if let Some(op) = op_code_num {
107                // Handle PUSHDATA opcodes specially (they have length and data following)
108                if op == OP_PUSHDATA1 || op == OP_PUSHDATA2 || op == OP_PUSHDATA4 {
109                    // Format: OP_PUSHDATA1 <length> <hex>
110                    if i + 2 < tokens.len() {
111                        let data = from_hex(tokens[i + 2])?;
112                        chunks.push(ScriptChunk::new(op, Some(data)));
113                        i += 3;
114                    } else {
115                        chunks.push(ScriptChunk::new_opcode(op));
116                        i += 1;
117                    }
118                } else {
119                    chunks.push(ScriptChunk::new_opcode(op));
120                    i += 1;
121                }
122            } else {
123                i += 1;
124            }
125        }
126
127        Ok(Self::from_chunks(chunks))
128    }
129
130    /// Constructs a Script instance from a hexadecimal string.
131    ///
132    /// # Example
133    ///
134    /// ```
135    /// use bsv_rs::script::Script;
136    ///
137    /// let script = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
138    /// ```
139    pub fn from_hex(hex: &str) -> Result<Self> {
140        if hex.is_empty() {
141            return Ok(Self::new());
142        }
143
144        let bin = from_hex(hex)?;
145        let raw_bytes = bin.clone();
146        let hex_lower = hex.to_lowercase();
147
148        Ok(Self {
149            chunks: RefCell::new(Vec::new()),
150            parsed: RefCell::new(false),
151            raw_bytes_cache: RefCell::new(Some(raw_bytes)),
152            hex_cache: RefCell::new(Some(hex_lower)),
153        })
154    }
155
156    /// Constructs a Script instance from binary data.
157    ///
158    /// # Example
159    ///
160    /// ```
161    /// use bsv_rs::script::Script;
162    ///
163    /// let script = Script::from_binary(&[0x76, 0xa9]).unwrap();
164    /// ```
165    pub fn from_binary(bin: &[u8]) -> Result<Self> {
166        Ok(Self {
167            chunks: RefCell::new(Vec::new()),
168            parsed: RefCell::new(false),
169            raw_bytes_cache: RefCell::new(Some(bin.to_vec())),
170            hex_cache: RefCell::new(None),
171        })
172    }
173
174    /// Ensures that the chunks have been parsed from raw bytes.
175    fn ensure_parsed(&self) {
176        if *self.parsed.borrow() {
177            return;
178        }
179
180        if let Some(ref bytes) = *self.raw_bytes_cache.borrow() {
181            *self.chunks.borrow_mut() = Self::parse_chunks(bytes);
182        }
183        *self.parsed.borrow_mut() = true;
184    }
185
186    /// Parses raw bytes into script chunks.
187    /// The chunk index of the first push that declares more bytes than the
188    /// script holds (a direct push, or a `OP_PUSHDATA1/2/4` whose length bytes
189    /// or data are cut short), or `None` when every push is complete.
190    ///
191    /// The parser is lenient (a truncated push yields a shorter chunk, as the
192    /// TypeScript SDK's does), but the reference refuses to READ such an
193    /// opcode: `GetOp` returns false (`script.h:190-191`) and the interpreter
194    /// fails the script with `SCRIPT_ERR_BAD_OPCODE` when its walk reaches it,
195    /// executed or not (`interpreter.cpp:450-451`). `Spend` applies that at
196    /// this index. Bytes after a top-level `OP_RETURN` are data on both sides
197    /// and are not examined.
198    pub fn truncated_push(&self) -> Option<usize> {
199        truncated_push_index(&self.to_binary())
200    }
201
202    fn parse_chunks(bytes: &[u8]) -> Vec<ScriptChunk> {
203        let mut chunks = Vec::new();
204        let length = bytes.len();
205        let mut pos = 0;
206        let mut in_conditional_block = 0i32;
207
208        while pos < length {
209            let op = bytes[pos];
210            pos += 1;
211
212            // Handle OP_RETURN outside conditional blocks
213            if op == OP_RETURN && in_conditional_block == 0 {
214                // Everything after OP_RETURN is data
215                let data = if pos < length {
216                    Some(bytes[pos..].to_vec())
217                } else {
218                    None
219                };
220                chunks.push(ScriptChunk::new(op, data));
221                break;
222            }
223
224            // Track conditional blocks
225            if op == OP_IF || op == OP_NOTIF || op == OP_VERIF || op == OP_VERNOTIF {
226                in_conditional_block += 1;
227            } else if op == OP_ENDIF {
228                in_conditional_block = (in_conditional_block - 1).max(0);
229            }
230
231            // Handle push operations
232            if op > 0 && op < OP_PUSHDATA1 {
233                let len = op as usize;
234                let end = (pos + len).min(length);
235                let data = bytes[pos..end].to_vec();
236                chunks.push(ScriptChunk::new(op, Some(data)));
237                pos = end;
238            } else if op == OP_PUSHDATA1 {
239                let len = if pos < length { bytes[pos] as usize } else { 0 };
240                // .min(length): a bare trailing 0x4c (truncated PUSHDATA1)
241                // previously advanced pos past the buffer and PANICKED on the
242                // start>end slice below — attacker-suppliable script bytes
243                // must never panic (conformance regression script-012;
244                // PUSHDATA2/4 arms already clamp).
245                pos = (pos + 1).min(length);
246                let end = (pos + len).min(length);
247                let data = bytes[pos..end].to_vec();
248                chunks.push(ScriptChunk::new(op, Some(data)));
249                pos = end;
250            } else if op == OP_PUSHDATA2 {
251                let b0 = if pos < length { bytes[pos] as usize } else { 0 };
252                let b1 = if pos + 1 < length {
253                    bytes[pos + 1] as usize
254                } else {
255                    0
256                };
257                let len = b0 | (b1 << 8);
258                pos = (pos + 2).min(length);
259                let end = (pos + len).min(length);
260                let data = bytes[pos..end].to_vec();
261                chunks.push(ScriptChunk::new(op, Some(data)));
262                pos = end;
263            } else if op == OP_PUSHDATA4 {
264                let b0 = if pos < length { bytes[pos] as u32 } else { 0 };
265                let b1 = if pos + 1 < length {
266                    bytes[pos + 1] as u32
267                } else {
268                    0
269                };
270                let b2 = if pos + 2 < length {
271                    bytes[pos + 2] as u32
272                } else {
273                    0
274                };
275                let b3 = if pos + 3 < length {
276                    bytes[pos + 3] as u32
277                } else {
278                    0
279                };
280                let len = (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) as usize;
281                pos = (pos + 4).min(length);
282                let end = (pos + len).min(length);
283                let data = bytes[pos..end].to_vec();
284                chunks.push(ScriptChunk::new(op, Some(data)));
285                pos = end;
286            } else {
287                chunks.push(ScriptChunk::new_opcode(op));
288            }
289        }
290
291        chunks
292    }
293
294    /// Serializes the script to an ASM formatted string.
295    pub fn to_asm(&self) -> String {
296        self.ensure_parsed();
297        let chunks = self.chunks.borrow();
298        let parts: Vec<String> = chunks.iter().map(|c| c.to_asm()).collect();
299        parts.join(" ")
300    }
301
302    /// Serializes the script to a hexadecimal string.
303    pub fn to_hex(&self) -> String {
304        if let Some(ref hex) = *self.hex_cache.borrow() {
305            return hex.clone();
306        }
307
308        let bytes = self.to_binary();
309        let hex = to_hex(&bytes);
310        *self.hex_cache.borrow_mut() = Some(hex.clone());
311        hex
312    }
313
314    /// Serializes the script to binary (byte array).
315    pub fn to_binary(&self) -> Vec<u8> {
316        if let Some(ref bytes) = *self.raw_bytes_cache.borrow() {
317            return bytes.clone();
318        }
319
320        self.ensure_parsed();
321        let bytes = self.serialize_chunks_to_bytes();
322        *self.raw_bytes_cache.borrow_mut() = Some(bytes.clone());
323        bytes
324    }
325
326    /// Computes the serialized length of the chunks.
327    fn compute_serialized_length(chunks: &[ScriptChunk]) -> usize {
328        let mut total = 0;
329        for chunk in chunks {
330            total += 1; // opcode byte
331            if let Some(ref data) = chunk.data {
332                let len = data.len();
333                if chunk.op == OP_RETURN {
334                    total += len;
335                    break;
336                }
337                if chunk.op < OP_PUSHDATA1 {
338                    total += len;
339                } else if chunk.op == OP_PUSHDATA1 {
340                    total += 1 + len;
341                } else if chunk.op == OP_PUSHDATA2 {
342                    total += 2 + len;
343                } else if chunk.op == OP_PUSHDATA4 {
344                    total += 4 + len;
345                }
346            }
347        }
348        total
349    }
350
351    /// Serializes chunks to raw bytes.
352    fn serialize_chunks_to_bytes(&self) -> Vec<u8> {
353        let chunks = self.chunks.borrow();
354        let total_length = Self::compute_serialized_length(&chunks);
355        let mut bytes = Vec::with_capacity(total_length);
356
357        for chunk in chunks.iter() {
358            bytes.push(chunk.op);
359
360            if let Some(ref data) = chunk.data {
361                if chunk.op == OP_RETURN {
362                    bytes.extend_from_slice(data);
363                    break;
364                }
365
366                if chunk.op < OP_PUSHDATA1 {
367                    bytes.extend_from_slice(data);
368                } else if chunk.op == OP_PUSHDATA1 {
369                    bytes.push(data.len() as u8);
370                    bytes.extend_from_slice(data);
371                } else if chunk.op == OP_PUSHDATA2 {
372                    let len = data.len() as u16;
373                    bytes.push((len & 0xff) as u8);
374                    bytes.push(((len >> 8) & 0xff) as u8);
375                    bytes.extend_from_slice(data);
376                } else if chunk.op == OP_PUSHDATA4 {
377                    let len = data.len() as u32;
378                    bytes.push((len & 0xff) as u8);
379                    bytes.push(((len >> 8) & 0xff) as u8);
380                    bytes.push(((len >> 16) & 0xff) as u8);
381                    bytes.push(((len >> 24) & 0xff) as u8);
382                    bytes.extend_from_slice(data);
383                }
384            }
385        }
386
387        bytes
388    }
389
390    /// Invalidates the serialization caches.
391    fn invalidate_caches(&self) {
392        *self.raw_bytes_cache.borrow_mut() = None;
393        *self.hex_cache.borrow_mut() = None;
394    }
395
396    /// Appends another script to this script.
397    pub fn write_script(&mut self, script: &Script) -> &mut Self {
398        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
399        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
400        // returned an EMPTY script (btg-token #11; the regression test names it).
401        self.ensure_parsed();
402        self.invalidate_caches();
403        script.ensure_parsed();
404        {
405            let mut chunks = self.chunks.borrow_mut();
406            chunks.extend(script.chunks.borrow().iter().cloned());
407        }
408        self
409    }
410
411    /// Appends an opcode to the script.
412    pub fn write_opcode(&mut self, op: u8) -> &mut Self {
413        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
414        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
415        // returned an EMPTY script (btg-token #11; the regression test names it).
416        self.ensure_parsed();
417        self.invalidate_caches();
418        self.chunks.borrow_mut().push(ScriptChunk::new_opcode(op));
419        self
420    }
421
422    /// Appends binary data to the script, determining the appropriate opcode based on length.
423    ///
424    /// # Panics
425    ///
426    /// Panics if `bin` is 4 GiB or longer: no push opcode carries it.
427    #[allow(clippy::panic)] // #134: the documented `# Panics` contract; a Result here is a breaking change
428    pub fn write_bin(&mut self, bin: &[u8]) -> &mut Self {
429        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
430        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
431        // returned an EMPTY script (btg-token #11; the regression test names it).
432        self.ensure_parsed();
433        self.invalidate_caches();
434
435        let len = bin.len();
436        let op: u8;
437        let data: Option<Vec<u8>>;
438
439        if len == 0 {
440            op = OP_0;
441            data = None;
442        } else if len < OP_PUSHDATA1 as usize {
443            op = len as u8;
444            data = Some(bin.to_vec());
445        } else if len < 256 {
446            op = OP_PUSHDATA1;
447            data = Some(bin.to_vec());
448        } else if len < 65536 {
449            op = OP_PUSHDATA2;
450            data = Some(bin.to_vec());
451        } else if (len as u64) < 0x100000000 {
452            op = OP_PUSHDATA4;
453            data = Some(bin.to_vec());
454        } else {
455            panic!("Data too large to push");
456        }
457
458        self.chunks.borrow_mut().push(ScriptChunk::new(op, data));
459        self
460    }
461
462    /// Appends a number to the script.
463    ///
464    /// Numbers 0, -1, and 1-16 use special opcodes.
465    /// Larger numbers are encoded in little-endian sign-magnitude format.
466    pub fn write_number(&mut self, num: i64) -> &mut Self {
467        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
468        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
469        // returned an EMPTY script (btg-token #11; the regression test names it).
470        self.ensure_parsed();
471        self.invalidate_caches();
472
473        if num == 0 {
474            self.chunks.borrow_mut().push(ScriptChunk::new_opcode(OP_0));
475        } else if num == -1 {
476            self.chunks
477                .borrow_mut()
478                .push(ScriptChunk::new_opcode(OP_1NEGATE));
479        } else if (1..=16).contains(&num) {
480            let op = (OP_1 as i64 + num - 1) as u8;
481            self.chunks.borrow_mut().push(ScriptChunk::new_opcode(op));
482        } else {
483            // Encode as little-endian sign-magnitude
484            let data = Self::encode_script_number(num);
485            let len = data.len();
486            let op = if len < OP_PUSHDATA1 as usize {
487                len as u8
488            } else if len < 256 {
489                OP_PUSHDATA1
490            } else {
491                OP_PUSHDATA2
492            };
493            self.chunks
494                .borrow_mut()
495                .push(ScriptChunk::new(op, Some(data)));
496        }
497
498        self
499    }
500
501    /// Encodes a number in Bitcoin script number format (little-endian sign-magnitude).
502    fn encode_script_number(num: i64) -> Vec<u8> {
503        if num == 0 {
504            return vec![];
505        }
506
507        let negative = num < 0;
508        let mut abs_val = num.unsigned_abs();
509        let mut result = Vec::new();
510
511        while abs_val > 0 {
512            result.push((abs_val & 0xff) as u8);
513            abs_val >>= 8;
514        }
515
516        // If the most significant byte has its high bit set, we need an extra byte
517        // to indicate the sign
518        if result.last().is_some_and(|&b| b & 0x80 != 0) {
519            result.push(if negative { 0x80 } else { 0x00 });
520        } else if negative {
521            // Set the sign bit on the most significant byte
522            if let Some(last) = result.last_mut() {
523                *last |= 0x80;
524            }
525        }
526
527        result
528    }
529
530    /// Removes all OP_CODESEPARATOR opcodes from the script.
531    pub fn remove_codeseparators(&mut self) -> &mut Self {
532        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
533        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
534        // returned an EMPTY script (btg-token #11; the regression test names it).
535        self.ensure_parsed();
536        self.invalidate_caches();
537
538        {
539            let mut chunks = self.chunks.borrow_mut();
540            chunks.retain(|chunk| chunk.op != OP_CODESEPARATOR);
541        }
542
543        self
544    }
545
546    /// Deletes the given script wherever it appears in the current script.
547    pub fn find_and_delete(&mut self, script: &Script) -> &mut Self {
548        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
549        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
550        // returned an EMPTY script (btg-token #11; the regression test names it).
551        self.ensure_parsed();
552        self.invalidate_caches();
553        script.ensure_parsed();
554
555        let target_hex = script.to_hex();
556        {
557            let mut chunks = self.chunks.borrow_mut();
558
559            chunks.retain(|chunk| {
560                let chunk_script = Script::from_chunks(vec![chunk.clone()]);
561                chunk_script.to_hex() != target_hex
562            });
563        }
564
565        self
566    }
567
568    /// Checks if the script contains only push data operations.
569    pub fn is_push_only(&self) -> bool {
570        self.ensure_parsed();
571        let chunks = self.chunks.borrow();
572        chunks.iter().all(|chunk| chunk.op <= OP_16)
573    }
574
575    /// Returns the chunks that make up this script.
576    pub fn chunks(&self) -> Vec<ScriptChunk> {
577        self.ensure_parsed();
578        self.chunks.borrow().clone()
579    }
580
581    /// Returns the number of chunks in this script.
582    pub fn len(&self) -> usize {
583        self.ensure_parsed();
584        self.chunks.borrow().len()
585    }
586
587    /// Returns true if this script is empty.
588    pub fn is_empty(&self) -> bool {
589        self.len() == 0
590    }
591
592    /// Sets a specific chunk's opcode at the given index.
593    pub fn set_chunk_opcode(&mut self, index: usize, op: u8) -> &mut Self {
594        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
595        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
596        // returned an EMPTY script (btg-token #11; the regression test names it).
597        self.ensure_parsed();
598        self.invalidate_caches();
599        {
600            let mut chunks = self.chunks.borrow_mut();
601            if index < chunks.len() {
602                chunks[index] = ScriptChunk::new_opcode(op);
603            }
604        }
605        self
606    }
607
608    /// Returns true if this script is a locking script.
609    /// Base Script always returns false; use LockingScript for true.
610    pub fn is_locking_script(&self) -> bool {
611        false
612    }
613
614    /// Returns true if this script is an unlocking script.
615    /// Base Script always returns false; use UnlockingScript for true.
616    pub fn is_unlocking_script(&self) -> bool {
617        false
618    }
619
620    /// Check if this is a Pay-to-Public-Key-Hash script
621    /// Pattern: OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
622    pub fn is_p2pkh(&self) -> bool {
623        let chunks = self.chunks();
624        chunks.len() == 5
625            && chunks[0].op == OP_DUP
626            && chunks[1].op == OP_HASH160
627            && chunks[2]
628                .data
629                .as_ref()
630                .map(|d| d.len() == 20)
631                .unwrap_or(false)
632            && chunks[3].op == OP_EQUALVERIFY
633            && chunks[4].op == OP_CHECKSIG
634    }
635
636    /// Check if this is a Pay-to-Public-Key script
637    /// Pattern: <33 or 65 bytes pubkey> OP_CHECKSIG
638    pub fn is_p2pk(&self) -> bool {
639        let chunks = self.chunks();
640        chunks.len() == 2
641            && chunks[0]
642                .data
643                .as_ref()
644                .map(|d| d.len() == 33 || d.len() == 65)
645                .unwrap_or(false)
646            && chunks[1].op == OP_CHECKSIG
647    }
648
649    /// Check if this is a Pay-to-Script-Hash script
650    /// Pattern: OP_HASH160 <20 bytes> OP_EQUAL
651    pub fn is_p2sh(&self) -> bool {
652        let chunks = self.chunks();
653        chunks.len() == 3
654            && chunks[0].op == OP_HASH160
655            && chunks[1]
656                .data
657                .as_ref()
658                .map(|d| d.len() == 20)
659                .unwrap_or(false)
660            && chunks[2].op == OP_EQUAL
661    }
662
663    /// Check if this is a data-only script (OP_RETURN or OP_FALSE OP_RETURN)
664    pub fn is_data(&self) -> bool {
665        let chunks = self.chunks();
666        if chunks.is_empty() {
667            return false;
668        }
669        // OP_RETURN ...
670        if chunks[0].op == OP_RETURN {
671            return true;
672        }
673        // OP_FALSE OP_RETURN ... (safe data carrier)
674        chunks.len() >= 2 && chunks[0].op == OP_FALSE && chunks[1].op == OP_RETURN
675    }
676
677    /// Check if this is a safe data carrier script (OP_FALSE OP_RETURN pattern).
678    ///
679    /// A safe data carrier is provably unspendable and prunable. Unlike bare
680    /// `OP_RETURN`, it won't be rejected by nodes even with large data payloads.
681    pub fn is_safe_data_carrier(&self) -> bool {
682        let chunks = self.chunks();
683        chunks.len() >= 2 && chunks[0].op == OP_FALSE && chunks[1].op == OP_RETURN
684    }
685
686    /// Check if this is a multisig script, returns (M, N) if so
687    /// Pattern: `OP_M <pubkey>... OP_N OP_CHECKMULTISIG`
688    pub fn is_multisig(&self) -> Option<(u8, u8)> {
689        let chunks = self.chunks();
690        if chunks.len() < 4 {
691            return None;
692        }
693
694        // Last opcode must be OP_CHECKMULTISIG
695        if chunks.last()?.op != OP_CHECKMULTISIG {
696            return None;
697        }
698
699        // Second to last must be N (OP_1 through OP_16)
700        let n = Self::opcode_to_small_int(chunks[chunks.len() - 2].op)?;
701
702        // First must be M (OP_1 through OP_16)
703        let m = Self::opcode_to_small_int(chunks[0].op)?;
704
705        // Verify we have exactly N pubkeys
706        if chunks.len() != (n as usize) + 3 {
707            return None;
708        }
709
710        // Verify all middle elements are pubkeys (33 or 65 bytes)
711        for chunk in chunks.iter().take((n as usize) + 1).skip(1) {
712            let data = chunk.data.as_ref()?;
713            if data.len() != 33 && data.len() != 65 {
714                return None;
715            }
716        }
717
718        Some((m, n))
719    }
720
721    /// Convert OP_1 through OP_16 to their numeric values
722    pub fn opcode_to_small_int(op: u8) -> Option<u8> {
723        if (OP_1..=OP_16).contains(&op) {
724            Some(op - OP_1 + 1)
725        } else {
726            None
727        }
728    }
729
730    /// Extract the public key hash from a P2PKH script
731    pub fn extract_pubkey_hash(&self) -> Option<[u8; 20]> {
732        if !self.is_p2pkh() {
733            return None;
734        }
735        let chunks = self.chunks();
736        let data = chunks[2].data.as_ref()?;
737        let mut hash = [0u8; 20];
738        hash.copy_from_slice(data);
739        Some(hash)
740    }
741
742    /// Extracts the public key from a P2PK script.
743    ///
744    /// Returns the raw public key bytes from the first chunk of a P2PK script
745    /// (`<pubkey> OP_CHECKSIG`), or `None` if this is not a P2PK script.
746    ///
747    /// Matches the Go SDK's `Script.PubKey()` method.
748    pub fn get_public_key(&self) -> Option<Vec<u8>> {
749        if !self.is_p2pk() {
750            return None;
751        }
752        let chunks = self.chunks();
753        chunks[0].data.clone()
754    }
755
756    /// Extracts the public key from a P2PK script as a hex string.
757    ///
758    /// Returns the hex-encoded public key from the first chunk of a P2PK script,
759    /// or `None` if this is not a P2PK script.
760    ///
761    /// Matches the Go SDK's `Script.PubKeyHex()` method.
762    pub fn get_public_key_hex(&self) -> Option<String> {
763        self.get_public_key()
764            .map(|bytes| crate::primitives::to_hex(&bytes))
765    }
766}
767
768impl Default for Script {
769    fn default() -> Self {
770        Self::new()
771    }
772}
773
774/// The index of the first truncated push in `bytes`, walking exactly as
775/// `Script::parse_chunks` walks (the same top-level `OP_RETURN` rule, the same
776/// conditional tracking), so the index names the same chunk.
777fn truncated_push_index(bytes: &[u8]) -> Option<usize> {
778    let length = bytes.len();
779    let mut pos = 0;
780    let mut index = 0usize;
781    let mut in_conditional_block = 0i32;
782    while pos < length {
783        let op = bytes[pos];
784        pos += 1;
785        if op == OP_RETURN && in_conditional_block == 0 {
786            return None;
787        }
788        if op == OP_IF || op == OP_NOTIF || op == OP_VERIF || op == OP_VERNOTIF {
789            in_conditional_block += 1;
790        } else if op == OP_ENDIF {
791            in_conditional_block = (in_conditional_block - 1).max(0);
792        }
793        let declared: Option<usize> = if op > 0 && op < OP_PUSHDATA1 {
794            Some(op as usize)
795        } else if op == OP_PUSHDATA1 {
796            if pos + 1 > length {
797                return Some(index);
798            }
799            let n = bytes[pos] as usize;
800            pos += 1;
801            Some(n)
802        } else if op == OP_PUSHDATA2 {
803            if pos + 2 > length {
804                return Some(index);
805            }
806            let n = bytes[pos] as usize | ((bytes[pos + 1] as usize) << 8);
807            pos += 2;
808            Some(n)
809        } else if op == OP_PUSHDATA4 {
810            if pos + 4 > length {
811                return Some(index);
812            }
813            let n = bytes[pos] as usize
814                | ((bytes[pos + 1] as usize) << 8)
815                | ((bytes[pos + 2] as usize) << 16)
816                | ((bytes[pos + 3] as usize) << 24);
817            pos += 4;
818            Some(n)
819        } else {
820            None
821        };
822        if let Some(n) = declared {
823            if pos + n > length {
824                return Some(index);
825            }
826            pos += n;
827        }
828        index += 1;
829    }
830    None
831}
832
833impl PartialEq for Script {
834    fn eq(&self, other: &Self) -> bool {
835        self.to_binary() == other.to_binary()
836    }
837}
838
839impl Eq for Script {}
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844
845    #[test]
846    fn test_new_empty_script() {
847        let script = Script::new();
848        assert!(script.is_empty());
849        assert_eq!(script.to_hex(), "");
850        assert_eq!(script.to_asm(), "");
851    }
852
853    #[test]
854    fn test_from_hex() {
855        // OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
856        let hex = "76a914000000000000000000000000000000000000000088ac";
857        let script = Script::from_hex(hex).unwrap();
858        assert_eq!(script.to_hex(), hex);
859    }
860
861    #[test]
862    fn test_from_binary() {
863        let bin = vec![0x76, 0xa9]; // OP_DUP OP_HASH160
864        let script = Script::from_binary(&bin).unwrap();
865        assert_eq!(script.to_binary(), bin);
866    }
867
868    #[test]
869    fn test_from_asm_simple() {
870        let asm = "OP_DUP OP_HASH160";
871        let script = Script::from_asm(asm).unwrap();
872        assert_eq!(script.to_hex(), "76a9");
873    }
874
875    #[test]
876    fn test_from_asm_with_data() {
877        let asm =
878            "OP_DUP OP_HASH160 0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG";
879        let script = Script::from_asm(asm).unwrap();
880        assert_eq!(
881            script.to_hex(),
882            "76a914000000000000000000000000000000000000000088ac"
883        );
884    }
885
886    #[test]
887    fn test_from_asm_special_values() {
888        let script = Script::from_asm("0").unwrap();
889        assert_eq!(script.to_hex(), "00");
890
891        let script = Script::from_asm("-1").unwrap();
892        assert_eq!(script.to_hex(), "4f");
893    }
894
895    #[test]
896    fn test_to_asm() {
897        let hex = "76a914000000000000000000000000000000000000000088ac";
898        let script = Script::from_hex(hex).unwrap();
899        let asm = script.to_asm();
900        assert!(asm.contains("OP_DUP"));
901        assert!(asm.contains("OP_HASH160"));
902        assert!(asm.contains("OP_EQUALVERIFY"));
903        assert!(asm.contains("OP_CHECKSIG"));
904    }
905
906    #[test]
907    fn test_write_opcode() {
908        let mut script = Script::new();
909        script.write_opcode(OP_DUP).write_opcode(OP_HASH160);
910        assert_eq!(script.to_hex(), "76a9");
911    }
912
913    #[test]
914    fn test_write_bin() {
915        let mut script = Script::new();
916        script.write_bin(&[0x01, 0x02, 0x03]);
917        assert_eq!(script.to_hex(), "03010203");
918    }
919
920    #[test]
921    fn test_write_bin_empty() {
922        let mut script = Script::new();
923        script.write_bin(&[]);
924        assert_eq!(script.to_hex(), "00"); // OP_0
925    }
926
927    #[test]
928    fn test_write_number() {
929        let mut script = Script::new();
930        script.write_number(0);
931        assert_eq!(script.to_hex(), "00"); // OP_0
932
933        let mut script = Script::new();
934        script.write_number(-1);
935        assert_eq!(script.to_hex(), "4f"); // OP_1NEGATE
936
937        let mut script = Script::new();
938        script.write_number(1);
939        assert_eq!(script.to_hex(), "51"); // OP_1
940
941        let mut script = Script::new();
942        script.write_number(16);
943        assert_eq!(script.to_hex(), "60"); // OP_16
944    }
945
946    #[test]
947    fn test_write_number_large() {
948        let mut script = Script::new();
949        script.write_number(127);
950        assert_eq!(script.to_hex(), "017f");
951
952        let mut script = Script::new();
953        script.write_number(128);
954        assert_eq!(script.to_hex(), "028000"); // Needs sign byte
955
956        let mut script = Script::new();
957        script.write_number(-127);
958        assert_eq!(script.to_hex(), "01ff");
959    }
960
961    #[test]
962    fn test_is_push_only() {
963        let script = Script::from_asm("OP_1 OP_2 OP_3").unwrap();
964        assert!(script.is_push_only());
965
966        let script = Script::from_asm("OP_DUP OP_HASH160").unwrap();
967        assert!(!script.is_push_only());
968    }
969
970    #[test]
971    fn test_remove_codeseparators() {
972        let mut script =
973            Script::from_asm("OP_DUP OP_CODESEPARATOR OP_HASH160 OP_CODESEPARATOR").unwrap();
974        script.remove_codeseparators();
975        assert_eq!(script.to_asm(), "OP_DUP OP_HASH160");
976    }
977
978    #[test]
979    fn test_find_and_delete() {
980        let mut script = Script::from_asm("OP_DUP OP_HASH160 OP_DUP").unwrap();
981        let target = Script::from_asm("OP_DUP").unwrap();
982        script.find_and_delete(&target);
983        assert_eq!(script.to_asm(), "OP_HASH160");
984    }
985
986    #[test]
987    fn test_chunks() {
988        let script = Script::from_asm("OP_DUP OP_HASH160").unwrap();
989        let chunks = script.chunks();
990        assert_eq!(chunks.len(), 2);
991        assert_eq!(chunks[0].op, OP_DUP);
992        assert_eq!(chunks[1].op, OP_HASH160);
993    }
994
995    #[test]
996    fn test_write_script() {
997        let mut script1 = Script::from_asm("OP_DUP").unwrap();
998        let script2 = Script::from_asm("OP_HASH160").unwrap();
999        script1.write_script(&script2);
1000        assert_eq!(script1.to_asm(), "OP_DUP OP_HASH160");
1001    }
1002
1003    #[test]
1004    fn test_op_return_parsing() {
1005        // OP_RETURN followed by data
1006        let hex = "6a0568656c6c6f"; // OP_RETURN <5 bytes "hello">
1007        let script = Script::from_hex(hex).unwrap();
1008        let chunks = script.chunks();
1009        assert_eq!(chunks.len(), 1);
1010        assert_eq!(chunks[0].op, OP_RETURN);
1011        // The data includes the push opcode and data
1012        assert!(chunks[0].data.is_some());
1013    }
1014
1015    #[test]
1016    fn test_pushdata1() {
1017        // Create a script with PUSHDATA1
1018        let data = vec![0u8; 100];
1019        let mut script = Script::new();
1020        script.write_bin(&data);
1021        let bytes = script.to_binary();
1022        assert_eq!(bytes[0], OP_PUSHDATA1);
1023        assert_eq!(bytes[1], 100);
1024        assert_eq!(&bytes[2..], &data[..]);
1025    }
1026
1027    #[test]
1028    fn test_pushdata2() {
1029        // Create a script with PUSHDATA2
1030        let data = vec![0u8; 300];
1031        let mut script = Script::new();
1032        script.write_bin(&data);
1033        let bytes = script.to_binary();
1034        assert_eq!(bytes[0], OP_PUSHDATA2);
1035        assert_eq!(bytes[1], (300 & 0xff) as u8);
1036        assert_eq!(bytes[2], ((300 >> 8) & 0xff) as u8);
1037        assert_eq!(&bytes[3..], &data[..]);
1038    }
1039
1040    #[test]
1041    fn test_roundtrip_hex() {
1042        let hex = "76a914000000000000000000000000000000000000000088ac";
1043        let script = Script::from_hex(hex).unwrap();
1044        assert_eq!(script.to_hex(), hex);
1045    }
1046
1047    #[test]
1048    fn test_equality() {
1049        let script1 = Script::from_hex("76a9").unwrap();
1050        let script2 = Script::from_asm("OP_DUP OP_HASH160").unwrap();
1051        assert_eq!(script1, script2);
1052    }
1053}
1054
1055#[cfg(test)]
1056mod script_type_tests {
1057    use super::*;
1058
1059    #[test]
1060    fn test_is_p2pkh() {
1061        // Valid P2PKH: OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
1062        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1063        assert!(p2pkh.is_p2pkh());
1064
1065        // Not P2PKH - wrong length pubkey hash
1066        let wrong_len = Script::from_hex("76a9140000000000000000000088ac").unwrap();
1067        assert!(!wrong_len.is_p2pkh());
1068
1069        // Not P2PKH - missing OP_DUP
1070        let no_dup = Script::from_hex("a914000000000000000000000000000000000000000088ac").unwrap();
1071        assert!(!no_dup.is_p2pkh());
1072
1073        // Empty script
1074        let empty = Script::new();
1075        assert!(!empty.is_p2pkh());
1076    }
1077
1078    #[test]
1079    fn test_is_p2pk() {
1080        // Valid P2PK with compressed pubkey (33 bytes)
1081        let mut p2pk_compressed = Script::new();
1082        p2pk_compressed.write_bin(&[0x02; 33]);
1083        p2pk_compressed.write_opcode(OP_CHECKSIG);
1084        assert!(p2pk_compressed.is_p2pk());
1085
1086        // Valid P2PK with uncompressed pubkey (65 bytes)
1087        let mut p2pk_uncompressed = Script::new();
1088        p2pk_uncompressed.write_bin(&[0x04; 65]);
1089        p2pk_uncompressed.write_opcode(OP_CHECKSIG);
1090        assert!(p2pk_uncompressed.is_p2pk());
1091
1092        // Invalid - wrong pubkey length
1093        let mut wrong_len = Script::new();
1094        wrong_len.write_bin(&[0x02; 32]);
1095        wrong_len.write_opcode(OP_CHECKSIG);
1096        assert!(!wrong_len.is_p2pk());
1097
1098        // Invalid - missing OP_CHECKSIG
1099        let mut no_checksig = Script::new();
1100        no_checksig.write_bin(&[0x02; 33]);
1101        assert!(!no_checksig.is_p2pk());
1102    }
1103
1104    #[test]
1105    fn test_is_p2sh() {
1106        // Valid P2SH: OP_HASH160 <20 bytes> OP_EQUAL
1107        let p2sh = Script::from_hex("a914000000000000000000000000000000000000000087").unwrap();
1108        assert!(p2sh.is_p2sh());
1109
1110        // Not P2SH - wrong hash length
1111        let wrong_len = Script::from_hex("a91400000000000000000087").unwrap();
1112        assert!(!wrong_len.is_p2sh());
1113
1114        // Not P2SH - OP_EQUALVERIFY instead of OP_EQUAL
1115        let equalverify =
1116            Script::from_hex("a914000000000000000000000000000000000000000088").unwrap();
1117        assert!(!equalverify.is_p2sh());
1118    }
1119
1120    #[test]
1121    fn test_is_data() {
1122        // OP_RETURN script
1123        let op_return = Script::from_asm("OP_RETURN").unwrap();
1124        assert!(op_return.is_data());
1125
1126        // OP_RETURN with data
1127        let mut op_return_data = Script::new();
1128        op_return_data.write_opcode(OP_RETURN);
1129        op_return_data.write_bin(b"hello");
1130        assert!(op_return_data.is_data());
1131
1132        // OP_FALSE OP_RETURN (safe data carrier)
1133        let safe_data = Script::from_asm("OP_FALSE OP_RETURN").unwrap();
1134        assert!(safe_data.is_data());
1135
1136        // Not a data script
1137        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1138        assert!(!p2pkh.is_data());
1139
1140        // Empty script
1141        let empty = Script::new();
1142        assert!(!empty.is_data());
1143    }
1144
1145    #[test]
1146    fn test_is_safe_data_carrier() {
1147        // OP_FALSE OP_RETURN is a safe data carrier
1148        let safe = Script::from_asm("OP_FALSE OP_RETURN").unwrap();
1149        assert!(safe.is_safe_data_carrier());
1150
1151        // OP_FALSE OP_RETURN with data is still safe
1152        let safe_data = Script::from_hex("006a0568656c6c6f").unwrap();
1153        assert!(safe_data.is_safe_data_carrier());
1154
1155        // Bare OP_RETURN is NOT a safe data carrier
1156        let bare = Script::from_asm("OP_RETURN").unwrap();
1157        assert!(!bare.is_safe_data_carrier());
1158
1159        // Empty script is not a safe data carrier
1160        let empty = Script::new();
1161        assert!(!empty.is_safe_data_carrier());
1162
1163        // P2PKH is not a safe data carrier
1164        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1165        assert!(!p2pkh.is_safe_data_carrier());
1166    }
1167
1168    #[test]
1169    fn test_is_multisig() {
1170        // 2-of-3 multisig: OP_2 <pk1> <pk2> <pk3> OP_3 OP_CHECKMULTISIG
1171        let mut multisig = Script::new();
1172        multisig.write_opcode(OP_2);
1173        multisig.write_bin(&[0x02; 33]); // pubkey 1
1174        multisig.write_bin(&[0x03; 33]); // pubkey 2
1175        multisig.write_bin(&[0x02; 33]); // pubkey 3
1176        multisig.write_opcode(OP_3);
1177        multisig.write_opcode(OP_CHECKMULTISIG);
1178
1179        let result = multisig.is_multisig();
1180        assert!(result.is_some());
1181        let (m, n) = result.unwrap();
1182        assert_eq!(m, 2);
1183        assert_eq!(n, 3);
1184
1185        // 1-of-1 multisig
1186        let mut one_of_one = Script::new();
1187        one_of_one.write_opcode(OP_1);
1188        one_of_one.write_bin(&[0x02; 33]);
1189        one_of_one.write_opcode(OP_1);
1190        one_of_one.write_opcode(OP_CHECKMULTISIG);
1191
1192        let result = one_of_one.is_multisig();
1193        assert!(result.is_some());
1194        let (m, n) = result.unwrap();
1195        assert_eq!(m, 1);
1196        assert_eq!(n, 1);
1197
1198        // Not multisig - P2PKH
1199        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1200        assert!(p2pkh.is_multisig().is_none());
1201
1202        // Not multisig - wrong number of pubkeys
1203        let mut wrong_count = Script::new();
1204        wrong_count.write_opcode(OP_2);
1205        wrong_count.write_bin(&[0x02; 33]); // only 1 pubkey
1206        wrong_count.write_opcode(OP_2);
1207        wrong_count.write_opcode(OP_CHECKMULTISIG);
1208        assert!(wrong_count.is_multisig().is_none());
1209    }
1210
1211    #[test]
1212    fn test_opcode_to_small_int() {
1213        assert_eq!(Script::opcode_to_small_int(OP_1), Some(1));
1214        assert_eq!(Script::opcode_to_small_int(OP_2), Some(2));
1215        assert_eq!(Script::opcode_to_small_int(OP_16), Some(16));
1216        assert_eq!(Script::opcode_to_small_int(OP_0), None);
1217        assert_eq!(Script::opcode_to_small_int(OP_DUP), None);
1218    }
1219
1220    #[test]
1221    fn test_extract_pubkey_hash() {
1222        // Valid P2PKH
1223        let p2pkh = Script::from_hex("76a914000102030405060708090a0b0c0d0e0f1011121388ac").unwrap();
1224        let hash = p2pkh.extract_pubkey_hash();
1225        assert!(hash.is_some());
1226        let hash = hash.unwrap();
1227        assert_eq!(
1228            hash,
1229            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
1230        );
1231
1232        // Not P2PKH
1233        let p2sh = Script::from_hex("a914000000000000000000000000000000000000000087").unwrap();
1234        assert!(p2sh.extract_pubkey_hash().is_none());
1235    }
1236
1237    #[test]
1238    fn test_is_push_only_comprehensive() {
1239        // Push-only scripts
1240        let push_only = Script::from_asm("OP_1 OP_2 OP_3").unwrap();
1241        assert!(push_only.is_push_only());
1242
1243        let data_push = Script::from_hex("03010203").unwrap(); // Push 3 bytes
1244        assert!(data_push.is_push_only());
1245
1246        // Not push-only
1247        let with_ops = Script::from_asm("OP_DUP OP_HASH160").unwrap();
1248        assert!(!with_ops.is_push_only());
1249    }
1250
1251    #[test]
1252    fn test_get_public_key() {
1253        // Valid P2PK with compressed pubkey (33 bytes)
1254        let pubkey_bytes = [0x02u8; 33];
1255        let mut p2pk = Script::new();
1256        p2pk.write_bin(&pubkey_bytes);
1257        p2pk.write_opcode(OP_CHECKSIG);
1258
1259        let result = p2pk.get_public_key();
1260        assert!(result.is_some());
1261        assert_eq!(result.unwrap(), pubkey_bytes.to_vec());
1262
1263        // Valid P2PK with uncompressed pubkey (65 bytes)
1264        let pubkey_bytes_uncompressed = [0x04u8; 65];
1265        let mut p2pk_uncompressed = Script::new();
1266        p2pk_uncompressed.write_bin(&pubkey_bytes_uncompressed);
1267        p2pk_uncompressed.write_opcode(OP_CHECKSIG);
1268
1269        let result = p2pk_uncompressed.get_public_key();
1270        assert!(result.is_some());
1271        assert_eq!(result.unwrap().len(), 65);
1272
1273        // Not P2PK - P2PKH script
1274        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1275        assert!(p2pkh.get_public_key().is_none());
1276
1277        // Not P2PK - empty script
1278        let empty = Script::new();
1279        assert!(empty.get_public_key().is_none());
1280    }
1281
1282    #[test]
1283    fn test_get_public_key_hex() {
1284        let pubkey_bytes = [0x02u8; 33];
1285        let mut p2pk = Script::new();
1286        p2pk.write_bin(&pubkey_bytes);
1287        p2pk.write_opcode(OP_CHECKSIG);
1288
1289        let hex = p2pk.get_public_key_hex();
1290        assert!(hex.is_some());
1291        assert_eq!(hex.unwrap(), crate::primitives::to_hex(&pubkey_bytes));
1292
1293        // Not P2PK
1294        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1295        assert!(p2pkh.get_public_key_hex().is_none());
1296    }
1297}