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    pub fn write_bin(&mut self, bin: &[u8]) -> &mut Self {
424        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
425        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
426        // returned an EMPTY script (btg-token #11; the regression test names it).
427        self.ensure_parsed();
428        self.invalidate_caches();
429
430        let len = bin.len();
431        let op: u8;
432        let data: Option<Vec<u8>>;
433
434        if len == 0 {
435            op = OP_0;
436            data = None;
437        } else if len < OP_PUSHDATA1 as usize {
438            op = len as u8;
439            data = Some(bin.to_vec());
440        } else if len < 256 {
441            op = OP_PUSHDATA1;
442            data = Some(bin.to_vec());
443        } else if len < 65536 {
444            op = OP_PUSHDATA2;
445            data = Some(bin.to_vec());
446        } else if (len as u64) < 0x100000000 {
447            op = OP_PUSHDATA4;
448            data = Some(bin.to_vec());
449        } else {
450            panic!("Data too large to push");
451        }
452
453        self.chunks.borrow_mut().push(ScriptChunk::new(op, data));
454        self
455    }
456
457    /// Appends a number to the script.
458    ///
459    /// Numbers 0, -1, and 1-16 use special opcodes.
460    /// Larger numbers are encoded in little-endian sign-magnitude format.
461    pub fn write_number(&mut self, num: i64) -> &mut Self {
462        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
463        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
464        // returned an EMPTY script (btg-token #11; the regression test names it).
465        self.ensure_parsed();
466        self.invalidate_caches();
467
468        if num == 0 {
469            self.chunks.borrow_mut().push(ScriptChunk::new_opcode(OP_0));
470        } else if num == -1 {
471            self.chunks
472                .borrow_mut()
473                .push(ScriptChunk::new_opcode(OP_1NEGATE));
474        } else if (1..=16).contains(&num) {
475            let op = (OP_1 as i64 + num - 1) as u8;
476            self.chunks.borrow_mut().push(ScriptChunk::new_opcode(op));
477        } else {
478            // Encode as little-endian sign-magnitude
479            let data = Self::encode_script_number(num);
480            let len = data.len();
481            let op = if len < OP_PUSHDATA1 as usize {
482                len as u8
483            } else if len < 256 {
484                OP_PUSHDATA1
485            } else {
486                OP_PUSHDATA2
487            };
488            self.chunks
489                .borrow_mut()
490                .push(ScriptChunk::new(op, Some(data)));
491        }
492
493        self
494    }
495
496    /// Encodes a number in Bitcoin script number format (little-endian sign-magnitude).
497    fn encode_script_number(num: i64) -> Vec<u8> {
498        if num == 0 {
499            return vec![];
500        }
501
502        let negative = num < 0;
503        let mut abs_val = num.unsigned_abs();
504        let mut result = Vec::new();
505
506        while abs_val > 0 {
507            result.push((abs_val & 0xff) as u8);
508            abs_val >>= 8;
509        }
510
511        // If the most significant byte has its high bit set, we need an extra byte
512        // to indicate the sign
513        if result.last().is_some_and(|&b| b & 0x80 != 0) {
514            result.push(if negative { 0x80 } else { 0x00 });
515        } else if negative {
516            // Set the sign bit on the most significant byte
517            if let Some(last) = result.last_mut() {
518                *last |= 0x80;
519            }
520        }
521
522        result
523    }
524
525    /// Removes all OP_CODESEPARATOR opcodes from the script.
526    pub fn remove_codeseparators(&mut self) -> &mut Self {
527        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
528        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
529        // returned an EMPTY script (btg-token #11; the regression test names it).
530        self.ensure_parsed();
531        self.invalidate_caches();
532
533        {
534            let mut chunks = self.chunks.borrow_mut();
535            chunks.retain(|chunk| chunk.op != OP_CODESEPARATOR);
536        }
537
538        self
539    }
540
541    /// Deletes the given script wherever it appears in the current script.
542    pub fn find_and_delete(&mut self, script: &Script) -> &mut Self {
543        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
544        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
545        // returned an EMPTY script (btg-token #11; the regression test names it).
546        self.ensure_parsed();
547        self.invalidate_caches();
548        script.ensure_parsed();
549
550        let target_hex = script.to_hex();
551        {
552            let mut chunks = self.chunks.borrow_mut();
553
554            chunks.retain(|chunk| {
555                let chunk_script = Script::from_chunks(vec![chunk.clone()]);
556                chunk_script.to_hex() != target_hex
557            });
558        }
559
560        self
561    }
562
563    /// Checks if the script contains only push data operations.
564    pub fn is_push_only(&self) -> bool {
565        self.ensure_parsed();
566        let chunks = self.chunks.borrow();
567        chunks.iter().all(|chunk| chunk.op <= OP_16)
568    }
569
570    /// Returns the chunks that make up this script.
571    pub fn chunks(&self) -> Vec<ScriptChunk> {
572        self.ensure_parsed();
573        self.chunks.borrow().clone()
574    }
575
576    /// Returns the number of chunks in this script.
577    pub fn len(&self) -> usize {
578        self.ensure_parsed();
579        self.chunks.borrow().len()
580    }
581
582    /// Returns true if this script is empty.
583    pub fn is_empty(&self) -> bool {
584        self.len() == 0
585    }
586
587    /// Sets a specific chunk's opcode at the given index.
588    pub fn set_chunk_opcode(&mut self, index: usize, op: u8) -> &mut Self {
589        // parse FIRST, then drop the byte cache: on a `from_binary` script the raw bytes are the
590        // only source of chunks, and the old order (invalidate, then parse) parsed from nothing and
591        // returned an EMPTY script (btg-token #11; the regression test names it).
592        self.ensure_parsed();
593        self.invalidate_caches();
594        {
595            let mut chunks = self.chunks.borrow_mut();
596            if index < chunks.len() {
597                chunks[index] = ScriptChunk::new_opcode(op);
598            }
599        }
600        self
601    }
602
603    /// Returns true if this script is a locking script.
604    /// Base Script always returns false; use LockingScript for true.
605    pub fn is_locking_script(&self) -> bool {
606        false
607    }
608
609    /// Returns true if this script is an unlocking script.
610    /// Base Script always returns false; use UnlockingScript for true.
611    pub fn is_unlocking_script(&self) -> bool {
612        false
613    }
614
615    /// Check if this is a Pay-to-Public-Key-Hash script
616    /// Pattern: OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
617    pub fn is_p2pkh(&self) -> bool {
618        let chunks = self.chunks();
619        chunks.len() == 5
620            && chunks[0].op == OP_DUP
621            && chunks[1].op == OP_HASH160
622            && chunks[2]
623                .data
624                .as_ref()
625                .map(|d| d.len() == 20)
626                .unwrap_or(false)
627            && chunks[3].op == OP_EQUALVERIFY
628            && chunks[4].op == OP_CHECKSIG
629    }
630
631    /// Check if this is a Pay-to-Public-Key script
632    /// Pattern: <33 or 65 bytes pubkey> OP_CHECKSIG
633    pub fn is_p2pk(&self) -> bool {
634        let chunks = self.chunks();
635        chunks.len() == 2
636            && chunks[0]
637                .data
638                .as_ref()
639                .map(|d| d.len() == 33 || d.len() == 65)
640                .unwrap_or(false)
641            && chunks[1].op == OP_CHECKSIG
642    }
643
644    /// Check if this is a Pay-to-Script-Hash script
645    /// Pattern: OP_HASH160 <20 bytes> OP_EQUAL
646    pub fn is_p2sh(&self) -> bool {
647        let chunks = self.chunks();
648        chunks.len() == 3
649            && chunks[0].op == OP_HASH160
650            && chunks[1]
651                .data
652                .as_ref()
653                .map(|d| d.len() == 20)
654                .unwrap_or(false)
655            && chunks[2].op == OP_EQUAL
656    }
657
658    /// Check if this is a data-only script (OP_RETURN or OP_FALSE OP_RETURN)
659    pub fn is_data(&self) -> bool {
660        let chunks = self.chunks();
661        if chunks.is_empty() {
662            return false;
663        }
664        // OP_RETURN ...
665        if chunks[0].op == OP_RETURN {
666            return true;
667        }
668        // OP_FALSE OP_RETURN ... (safe data carrier)
669        chunks.len() >= 2 && chunks[0].op == OP_FALSE && chunks[1].op == OP_RETURN
670    }
671
672    /// Check if this is a safe data carrier script (OP_FALSE OP_RETURN pattern).
673    ///
674    /// A safe data carrier is provably unspendable and prunable. Unlike bare
675    /// `OP_RETURN`, it won't be rejected by nodes even with large data payloads.
676    pub fn is_safe_data_carrier(&self) -> bool {
677        let chunks = self.chunks();
678        chunks.len() >= 2 && chunks[0].op == OP_FALSE && chunks[1].op == OP_RETURN
679    }
680
681    /// Check if this is a multisig script, returns (M, N) if so
682    /// Pattern: `OP_M <pubkey>... OP_N OP_CHECKMULTISIG`
683    pub fn is_multisig(&self) -> Option<(u8, u8)> {
684        let chunks = self.chunks();
685        if chunks.len() < 4 {
686            return None;
687        }
688
689        // Last opcode must be OP_CHECKMULTISIG
690        if chunks.last()?.op != OP_CHECKMULTISIG {
691            return None;
692        }
693
694        // Second to last must be N (OP_1 through OP_16)
695        let n = Self::opcode_to_small_int(chunks[chunks.len() - 2].op)?;
696
697        // First must be M (OP_1 through OP_16)
698        let m = Self::opcode_to_small_int(chunks[0].op)?;
699
700        // Verify we have exactly N pubkeys
701        if chunks.len() != (n as usize) + 3 {
702            return None;
703        }
704
705        // Verify all middle elements are pubkeys (33 or 65 bytes)
706        for chunk in chunks.iter().take((n as usize) + 1).skip(1) {
707            let data = chunk.data.as_ref()?;
708            if data.len() != 33 && data.len() != 65 {
709                return None;
710            }
711        }
712
713        Some((m, n))
714    }
715
716    /// Convert OP_1 through OP_16 to their numeric values
717    pub fn opcode_to_small_int(op: u8) -> Option<u8> {
718        if (OP_1..=OP_16).contains(&op) {
719            Some(op - OP_1 + 1)
720        } else {
721            None
722        }
723    }
724
725    /// Extract the public key hash from a P2PKH script
726    pub fn extract_pubkey_hash(&self) -> Option<[u8; 20]> {
727        if !self.is_p2pkh() {
728            return None;
729        }
730        let chunks = self.chunks();
731        let data = chunks[2].data.as_ref()?;
732        let mut hash = [0u8; 20];
733        hash.copy_from_slice(data);
734        Some(hash)
735    }
736
737    /// Extracts the public key from a P2PK script.
738    ///
739    /// Returns the raw public key bytes from the first chunk of a P2PK script
740    /// (`<pubkey> OP_CHECKSIG`), or `None` if this is not a P2PK script.
741    ///
742    /// Matches the Go SDK's `Script.PubKey()` method.
743    pub fn get_public_key(&self) -> Option<Vec<u8>> {
744        if !self.is_p2pk() {
745            return None;
746        }
747        let chunks = self.chunks();
748        chunks[0].data.clone()
749    }
750
751    /// Extracts the public key from a P2PK script as a hex string.
752    ///
753    /// Returns the hex-encoded public key from the first chunk of a P2PK script,
754    /// or `None` if this is not a P2PK script.
755    ///
756    /// Matches the Go SDK's `Script.PubKeyHex()` method.
757    pub fn get_public_key_hex(&self) -> Option<String> {
758        self.get_public_key()
759            .map(|bytes| crate::primitives::to_hex(&bytes))
760    }
761}
762
763impl Default for Script {
764    fn default() -> Self {
765        Self::new()
766    }
767}
768
769/// The index of the first truncated push in `bytes`, walking exactly as
770/// `Script::parse_chunks` walks (the same top-level `OP_RETURN` rule, the same
771/// conditional tracking), so the index names the same chunk.
772fn truncated_push_index(bytes: &[u8]) -> Option<usize> {
773    let length = bytes.len();
774    let mut pos = 0;
775    let mut index = 0usize;
776    let mut in_conditional_block = 0i32;
777    while pos < length {
778        let op = bytes[pos];
779        pos += 1;
780        if op == OP_RETURN && in_conditional_block == 0 {
781            return None;
782        }
783        if op == OP_IF || op == OP_NOTIF || op == OP_VERIF || op == OP_VERNOTIF {
784            in_conditional_block += 1;
785        } else if op == OP_ENDIF {
786            in_conditional_block = (in_conditional_block - 1).max(0);
787        }
788        let declared: Option<usize> = if op > 0 && op < OP_PUSHDATA1 {
789            Some(op as usize)
790        } else if op == OP_PUSHDATA1 {
791            if pos + 1 > length {
792                return Some(index);
793            }
794            let n = bytes[pos] as usize;
795            pos += 1;
796            Some(n)
797        } else if op == OP_PUSHDATA2 {
798            if pos + 2 > length {
799                return Some(index);
800            }
801            let n = bytes[pos] as usize | ((bytes[pos + 1] as usize) << 8);
802            pos += 2;
803            Some(n)
804        } else if op == OP_PUSHDATA4 {
805            if pos + 4 > length {
806                return Some(index);
807            }
808            let n = bytes[pos] as usize
809                | ((bytes[pos + 1] as usize) << 8)
810                | ((bytes[pos + 2] as usize) << 16)
811                | ((bytes[pos + 3] as usize) << 24);
812            pos += 4;
813            Some(n)
814        } else {
815            None
816        };
817        if let Some(n) = declared {
818            if pos + n > length {
819                return Some(index);
820            }
821            pos += n;
822        }
823        index += 1;
824    }
825    None
826}
827
828impl PartialEq for Script {
829    fn eq(&self, other: &Self) -> bool {
830        self.to_binary() == other.to_binary()
831    }
832}
833
834impl Eq for Script {}
835
836#[cfg(test)]
837mod tests {
838    use super::*;
839
840    #[test]
841    fn test_new_empty_script() {
842        let script = Script::new();
843        assert!(script.is_empty());
844        assert_eq!(script.to_hex(), "");
845        assert_eq!(script.to_asm(), "");
846    }
847
848    #[test]
849    fn test_from_hex() {
850        // OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
851        let hex = "76a914000000000000000000000000000000000000000088ac";
852        let script = Script::from_hex(hex).unwrap();
853        assert_eq!(script.to_hex(), hex);
854    }
855
856    #[test]
857    fn test_from_binary() {
858        let bin = vec![0x76, 0xa9]; // OP_DUP OP_HASH160
859        let script = Script::from_binary(&bin).unwrap();
860        assert_eq!(script.to_binary(), bin);
861    }
862
863    #[test]
864    fn test_from_asm_simple() {
865        let asm = "OP_DUP OP_HASH160";
866        let script = Script::from_asm(asm).unwrap();
867        assert_eq!(script.to_hex(), "76a9");
868    }
869
870    #[test]
871    fn test_from_asm_with_data() {
872        let asm =
873            "OP_DUP OP_HASH160 0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG";
874        let script = Script::from_asm(asm).unwrap();
875        assert_eq!(
876            script.to_hex(),
877            "76a914000000000000000000000000000000000000000088ac"
878        );
879    }
880
881    #[test]
882    fn test_from_asm_special_values() {
883        let script = Script::from_asm("0").unwrap();
884        assert_eq!(script.to_hex(), "00");
885
886        let script = Script::from_asm("-1").unwrap();
887        assert_eq!(script.to_hex(), "4f");
888    }
889
890    #[test]
891    fn test_to_asm() {
892        let hex = "76a914000000000000000000000000000000000000000088ac";
893        let script = Script::from_hex(hex).unwrap();
894        let asm = script.to_asm();
895        assert!(asm.contains("OP_DUP"));
896        assert!(asm.contains("OP_HASH160"));
897        assert!(asm.contains("OP_EQUALVERIFY"));
898        assert!(asm.contains("OP_CHECKSIG"));
899    }
900
901    #[test]
902    fn test_write_opcode() {
903        let mut script = Script::new();
904        script.write_opcode(OP_DUP).write_opcode(OP_HASH160);
905        assert_eq!(script.to_hex(), "76a9");
906    }
907
908    #[test]
909    fn test_write_bin() {
910        let mut script = Script::new();
911        script.write_bin(&[0x01, 0x02, 0x03]);
912        assert_eq!(script.to_hex(), "03010203");
913    }
914
915    #[test]
916    fn test_write_bin_empty() {
917        let mut script = Script::new();
918        script.write_bin(&[]);
919        assert_eq!(script.to_hex(), "00"); // OP_0
920    }
921
922    #[test]
923    fn test_write_number() {
924        let mut script = Script::new();
925        script.write_number(0);
926        assert_eq!(script.to_hex(), "00"); // OP_0
927
928        let mut script = Script::new();
929        script.write_number(-1);
930        assert_eq!(script.to_hex(), "4f"); // OP_1NEGATE
931
932        let mut script = Script::new();
933        script.write_number(1);
934        assert_eq!(script.to_hex(), "51"); // OP_1
935
936        let mut script = Script::new();
937        script.write_number(16);
938        assert_eq!(script.to_hex(), "60"); // OP_16
939    }
940
941    #[test]
942    fn test_write_number_large() {
943        let mut script = Script::new();
944        script.write_number(127);
945        assert_eq!(script.to_hex(), "017f");
946
947        let mut script = Script::new();
948        script.write_number(128);
949        assert_eq!(script.to_hex(), "028000"); // Needs sign byte
950
951        let mut script = Script::new();
952        script.write_number(-127);
953        assert_eq!(script.to_hex(), "01ff");
954    }
955
956    #[test]
957    fn test_is_push_only() {
958        let script = Script::from_asm("OP_1 OP_2 OP_3").unwrap();
959        assert!(script.is_push_only());
960
961        let script = Script::from_asm("OP_DUP OP_HASH160").unwrap();
962        assert!(!script.is_push_only());
963    }
964
965    #[test]
966    fn test_remove_codeseparators() {
967        let mut script =
968            Script::from_asm("OP_DUP OP_CODESEPARATOR OP_HASH160 OP_CODESEPARATOR").unwrap();
969        script.remove_codeseparators();
970        assert_eq!(script.to_asm(), "OP_DUP OP_HASH160");
971    }
972
973    #[test]
974    fn test_find_and_delete() {
975        let mut script = Script::from_asm("OP_DUP OP_HASH160 OP_DUP").unwrap();
976        let target = Script::from_asm("OP_DUP").unwrap();
977        script.find_and_delete(&target);
978        assert_eq!(script.to_asm(), "OP_HASH160");
979    }
980
981    #[test]
982    fn test_chunks() {
983        let script = Script::from_asm("OP_DUP OP_HASH160").unwrap();
984        let chunks = script.chunks();
985        assert_eq!(chunks.len(), 2);
986        assert_eq!(chunks[0].op, OP_DUP);
987        assert_eq!(chunks[1].op, OP_HASH160);
988    }
989
990    #[test]
991    fn test_write_script() {
992        let mut script1 = Script::from_asm("OP_DUP").unwrap();
993        let script2 = Script::from_asm("OP_HASH160").unwrap();
994        script1.write_script(&script2);
995        assert_eq!(script1.to_asm(), "OP_DUP OP_HASH160");
996    }
997
998    #[test]
999    fn test_op_return_parsing() {
1000        // OP_RETURN followed by data
1001        let hex = "6a0568656c6c6f"; // OP_RETURN <5 bytes "hello">
1002        let script = Script::from_hex(hex).unwrap();
1003        let chunks = script.chunks();
1004        assert_eq!(chunks.len(), 1);
1005        assert_eq!(chunks[0].op, OP_RETURN);
1006        // The data includes the push opcode and data
1007        assert!(chunks[0].data.is_some());
1008    }
1009
1010    #[test]
1011    fn test_pushdata1() {
1012        // Create a script with PUSHDATA1
1013        let data = vec![0u8; 100];
1014        let mut script = Script::new();
1015        script.write_bin(&data);
1016        let bytes = script.to_binary();
1017        assert_eq!(bytes[0], OP_PUSHDATA1);
1018        assert_eq!(bytes[1], 100);
1019        assert_eq!(&bytes[2..], &data[..]);
1020    }
1021
1022    #[test]
1023    fn test_pushdata2() {
1024        // Create a script with PUSHDATA2
1025        let data = vec![0u8; 300];
1026        let mut script = Script::new();
1027        script.write_bin(&data);
1028        let bytes = script.to_binary();
1029        assert_eq!(bytes[0], OP_PUSHDATA2);
1030        assert_eq!(bytes[1], (300 & 0xff) as u8);
1031        assert_eq!(bytes[2], ((300 >> 8) & 0xff) as u8);
1032        assert_eq!(&bytes[3..], &data[..]);
1033    }
1034
1035    #[test]
1036    fn test_roundtrip_hex() {
1037        let hex = "76a914000000000000000000000000000000000000000088ac";
1038        let script = Script::from_hex(hex).unwrap();
1039        assert_eq!(script.to_hex(), hex);
1040    }
1041
1042    #[test]
1043    fn test_equality() {
1044        let script1 = Script::from_hex("76a9").unwrap();
1045        let script2 = Script::from_asm("OP_DUP OP_HASH160").unwrap();
1046        assert_eq!(script1, script2);
1047    }
1048}
1049
1050#[cfg(test)]
1051mod script_type_tests {
1052    use super::*;
1053
1054    #[test]
1055    fn test_is_p2pkh() {
1056        // Valid P2PKH: OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
1057        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1058        assert!(p2pkh.is_p2pkh());
1059
1060        // Not P2PKH - wrong length pubkey hash
1061        let wrong_len = Script::from_hex("76a9140000000000000000000088ac").unwrap();
1062        assert!(!wrong_len.is_p2pkh());
1063
1064        // Not P2PKH - missing OP_DUP
1065        let no_dup = Script::from_hex("a914000000000000000000000000000000000000000088ac").unwrap();
1066        assert!(!no_dup.is_p2pkh());
1067
1068        // Empty script
1069        let empty = Script::new();
1070        assert!(!empty.is_p2pkh());
1071    }
1072
1073    #[test]
1074    fn test_is_p2pk() {
1075        // Valid P2PK with compressed pubkey (33 bytes)
1076        let mut p2pk_compressed = Script::new();
1077        p2pk_compressed.write_bin(&[0x02; 33]);
1078        p2pk_compressed.write_opcode(OP_CHECKSIG);
1079        assert!(p2pk_compressed.is_p2pk());
1080
1081        // Valid P2PK with uncompressed pubkey (65 bytes)
1082        let mut p2pk_uncompressed = Script::new();
1083        p2pk_uncompressed.write_bin(&[0x04; 65]);
1084        p2pk_uncompressed.write_opcode(OP_CHECKSIG);
1085        assert!(p2pk_uncompressed.is_p2pk());
1086
1087        // Invalid - wrong pubkey length
1088        let mut wrong_len = Script::new();
1089        wrong_len.write_bin(&[0x02; 32]);
1090        wrong_len.write_opcode(OP_CHECKSIG);
1091        assert!(!wrong_len.is_p2pk());
1092
1093        // Invalid - missing OP_CHECKSIG
1094        let mut no_checksig = Script::new();
1095        no_checksig.write_bin(&[0x02; 33]);
1096        assert!(!no_checksig.is_p2pk());
1097    }
1098
1099    #[test]
1100    fn test_is_p2sh() {
1101        // Valid P2SH: OP_HASH160 <20 bytes> OP_EQUAL
1102        let p2sh = Script::from_hex("a914000000000000000000000000000000000000000087").unwrap();
1103        assert!(p2sh.is_p2sh());
1104
1105        // Not P2SH - wrong hash length
1106        let wrong_len = Script::from_hex("a91400000000000000000087").unwrap();
1107        assert!(!wrong_len.is_p2sh());
1108
1109        // Not P2SH - OP_EQUALVERIFY instead of OP_EQUAL
1110        let equalverify =
1111            Script::from_hex("a914000000000000000000000000000000000000000088").unwrap();
1112        assert!(!equalverify.is_p2sh());
1113    }
1114
1115    #[test]
1116    fn test_is_data() {
1117        // OP_RETURN script
1118        let op_return = Script::from_asm("OP_RETURN").unwrap();
1119        assert!(op_return.is_data());
1120
1121        // OP_RETURN with data
1122        let mut op_return_data = Script::new();
1123        op_return_data.write_opcode(OP_RETURN);
1124        op_return_data.write_bin(b"hello");
1125        assert!(op_return_data.is_data());
1126
1127        // OP_FALSE OP_RETURN (safe data carrier)
1128        let safe_data = Script::from_asm("OP_FALSE OP_RETURN").unwrap();
1129        assert!(safe_data.is_data());
1130
1131        // Not a data script
1132        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1133        assert!(!p2pkh.is_data());
1134
1135        // Empty script
1136        let empty = Script::new();
1137        assert!(!empty.is_data());
1138    }
1139
1140    #[test]
1141    fn test_is_safe_data_carrier() {
1142        // OP_FALSE OP_RETURN is a safe data carrier
1143        let safe = Script::from_asm("OP_FALSE OP_RETURN").unwrap();
1144        assert!(safe.is_safe_data_carrier());
1145
1146        // OP_FALSE OP_RETURN with data is still safe
1147        let safe_data = Script::from_hex("006a0568656c6c6f").unwrap();
1148        assert!(safe_data.is_safe_data_carrier());
1149
1150        // Bare OP_RETURN is NOT a safe data carrier
1151        let bare = Script::from_asm("OP_RETURN").unwrap();
1152        assert!(!bare.is_safe_data_carrier());
1153
1154        // Empty script is not a safe data carrier
1155        let empty = Script::new();
1156        assert!(!empty.is_safe_data_carrier());
1157
1158        // P2PKH is not a safe data carrier
1159        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1160        assert!(!p2pkh.is_safe_data_carrier());
1161    }
1162
1163    #[test]
1164    fn test_is_multisig() {
1165        // 2-of-3 multisig: OP_2 <pk1> <pk2> <pk3> OP_3 OP_CHECKMULTISIG
1166        let mut multisig = Script::new();
1167        multisig.write_opcode(OP_2);
1168        multisig.write_bin(&[0x02; 33]); // pubkey 1
1169        multisig.write_bin(&[0x03; 33]); // pubkey 2
1170        multisig.write_bin(&[0x02; 33]); // pubkey 3
1171        multisig.write_opcode(OP_3);
1172        multisig.write_opcode(OP_CHECKMULTISIG);
1173
1174        let result = multisig.is_multisig();
1175        assert!(result.is_some());
1176        let (m, n) = result.unwrap();
1177        assert_eq!(m, 2);
1178        assert_eq!(n, 3);
1179
1180        // 1-of-1 multisig
1181        let mut one_of_one = Script::new();
1182        one_of_one.write_opcode(OP_1);
1183        one_of_one.write_bin(&[0x02; 33]);
1184        one_of_one.write_opcode(OP_1);
1185        one_of_one.write_opcode(OP_CHECKMULTISIG);
1186
1187        let result = one_of_one.is_multisig();
1188        assert!(result.is_some());
1189        let (m, n) = result.unwrap();
1190        assert_eq!(m, 1);
1191        assert_eq!(n, 1);
1192
1193        // Not multisig - P2PKH
1194        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1195        assert!(p2pkh.is_multisig().is_none());
1196
1197        // Not multisig - wrong number of pubkeys
1198        let mut wrong_count = Script::new();
1199        wrong_count.write_opcode(OP_2);
1200        wrong_count.write_bin(&[0x02; 33]); // only 1 pubkey
1201        wrong_count.write_opcode(OP_2);
1202        wrong_count.write_opcode(OP_CHECKMULTISIG);
1203        assert!(wrong_count.is_multisig().is_none());
1204    }
1205
1206    #[test]
1207    fn test_opcode_to_small_int() {
1208        assert_eq!(Script::opcode_to_small_int(OP_1), Some(1));
1209        assert_eq!(Script::opcode_to_small_int(OP_2), Some(2));
1210        assert_eq!(Script::opcode_to_small_int(OP_16), Some(16));
1211        assert_eq!(Script::opcode_to_small_int(OP_0), None);
1212        assert_eq!(Script::opcode_to_small_int(OP_DUP), None);
1213    }
1214
1215    #[test]
1216    fn test_extract_pubkey_hash() {
1217        // Valid P2PKH
1218        let p2pkh = Script::from_hex("76a914000102030405060708090a0b0c0d0e0f1011121388ac").unwrap();
1219        let hash = p2pkh.extract_pubkey_hash();
1220        assert!(hash.is_some());
1221        let hash = hash.unwrap();
1222        assert_eq!(
1223            hash,
1224            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
1225        );
1226
1227        // Not P2PKH
1228        let p2sh = Script::from_hex("a914000000000000000000000000000000000000000087").unwrap();
1229        assert!(p2sh.extract_pubkey_hash().is_none());
1230    }
1231
1232    #[test]
1233    fn test_is_push_only_comprehensive() {
1234        // Push-only scripts
1235        let push_only = Script::from_asm("OP_1 OP_2 OP_3").unwrap();
1236        assert!(push_only.is_push_only());
1237
1238        let data_push = Script::from_hex("03010203").unwrap(); // Push 3 bytes
1239        assert!(data_push.is_push_only());
1240
1241        // Not push-only
1242        let with_ops = Script::from_asm("OP_DUP OP_HASH160").unwrap();
1243        assert!(!with_ops.is_push_only());
1244    }
1245
1246    #[test]
1247    fn test_get_public_key() {
1248        // Valid P2PK with compressed pubkey (33 bytes)
1249        let pubkey_bytes = [0x02u8; 33];
1250        let mut p2pk = Script::new();
1251        p2pk.write_bin(&pubkey_bytes);
1252        p2pk.write_opcode(OP_CHECKSIG);
1253
1254        let result = p2pk.get_public_key();
1255        assert!(result.is_some());
1256        assert_eq!(result.unwrap(), pubkey_bytes.to_vec());
1257
1258        // Valid P2PK with uncompressed pubkey (65 bytes)
1259        let pubkey_bytes_uncompressed = [0x04u8; 65];
1260        let mut p2pk_uncompressed = Script::new();
1261        p2pk_uncompressed.write_bin(&pubkey_bytes_uncompressed);
1262        p2pk_uncompressed.write_opcode(OP_CHECKSIG);
1263
1264        let result = p2pk_uncompressed.get_public_key();
1265        assert!(result.is_some());
1266        assert_eq!(result.unwrap().len(), 65);
1267
1268        // Not P2PK - P2PKH script
1269        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1270        assert!(p2pkh.get_public_key().is_none());
1271
1272        // Not P2PK - empty script
1273        let empty = Script::new();
1274        assert!(empty.get_public_key().is_none());
1275    }
1276
1277    #[test]
1278    fn test_get_public_key_hex() {
1279        let pubkey_bytes = [0x02u8; 33];
1280        let mut p2pk = Script::new();
1281        p2pk.write_bin(&pubkey_bytes);
1282        p2pk.write_opcode(OP_CHECKSIG);
1283
1284        let hex = p2pk.get_public_key_hex();
1285        assert!(hex.is_some());
1286        assert_eq!(hex.unwrap(), crate::primitives::to_hex(&pubkey_bytes));
1287
1288        // Not P2PK
1289        let p2pkh = Script::from_hex("76a914000000000000000000000000000000000000000088ac").unwrap();
1290        assert!(p2pkh.get_public_key_hex().is_none());
1291    }
1292}