66# BRC-106 compliance: Opcode aliases for parsing
77# Build a comprehensive mapping of all opcode names (including aliases) to their byte values
88OPCODE_ALIASES = {
9- ' OP_FALSE' : b' \x00 ' ,
10- ' OP_0' : b' \x00 ' ,
11- ' OP_TRUE' : b' \x51 ' ,
12- ' OP_1' : b' \x51 '
9+ " OP_FALSE" : b" \x00 " ,
10+ " OP_0" : b" \x00 " ,
11+ " OP_TRUE" : b" \x51 " ,
12+ " OP_1" : b" \x51 " ,
1313}
1414
1515# Build name->value mapping for all OpCodes
1616OPCODE_NAME_VALUE_DICT = {item .name : item .value for item in OpCode }
1717# Merge with aliases
1818OPCODE_NAME_VALUE_DICT .update (OPCODE_ALIASES )
1919
20+
2021class ScriptChunk :
2122 """
2223 A representation of a chunk of a script, which includes an opcode.
@@ -43,51 +44,87 @@ def __init__(self, script: Union[str, bytes, None] = None):
4344 Create script from hex string or bytes
4445 """
4546 if script is None :
46- self .script : bytes = b''
47+ self ._bytes : bytes = b""
4748 elif isinstance (script , str ):
4849 # script in hex string
49- self .script : bytes = bytes .fromhex (script )
50+ self ._bytes : bytes = bytes .fromhex (script )
5051 elif isinstance (script , bytes ):
5152 # script in bytes
52- self .script : bytes = script
53+ self ._bytes : bytes = script
5354 else :
54- raise TypeError (' unsupported script type' )
55+ raise TypeError (" unsupported script type" )
5556 # An array of script chunks that make up the script.
5657 self .chunks : List [ScriptChunk ] = []
5758 self ._build_chunks ()
5859
60+ def _update_conditional_depth (self , op : bytes , depth : int ) -> int :
61+ """Update conditional block depth based on opcode."""
62+ if (
63+ op == OpCode .OP_IF
64+ or op == OpCode .OP_NOTIF
65+ or op == OpCode .OP_VERIF
66+ or op == OpCode .OP_VERNOTIF
67+ ):
68+ return depth + 1
69+ if op == OpCode .OP_ENDIF :
70+ return max (0 , depth - 1 )
71+ return depth
72+
73+ def _handle_op_return (self , reader : Reader , chunk : ScriptChunk ) -> bool :
74+ """Handle OP_RETURN opcode. Returns True if parsing should terminate."""
75+ remaining_length = len (reader .getvalue ()) - reader .tell ()
76+ if remaining_length > 0 :
77+ chunk .data = reader .read_bytes (remaining_length )
78+ else :
79+ chunk .data = None
80+ self .chunks .append (chunk )
81+ return True # Terminate parsing
82+
83+ def _read_push_data (self , reader : Reader , op : bytes ) -> Optional [bytes ]:
84+ """Read push data based on opcode. Returns data bytes or None."""
85+ if b"\x01 " <= op <= b"\x4b " :
86+ return reader .read_bytes (int .from_bytes (op , "big" ))
87+ if op == OpCode .OP_PUSHDATA1 :
88+ length = reader .read_uint8 ()
89+ return reader .read_bytes (length ) if length is not None else None
90+ if op == OpCode .OP_PUSHDATA2 :
91+ length = reader .read_uint16_le ()
92+ return reader .read_bytes (length ) if length is not None else None
93+ if op == OpCode .OP_PUSHDATA4 :
94+ length = reader .read_uint32_le ()
95+ return reader .read_bytes (length ) if length is not None else None
96+ return None
97+
5998 def _build_chunks (self ):
6099 self .chunks = []
61- reader = Reader (self .script )
100+ reader = Reader (self ._bytes )
101+ in_conditional_block = 0
102+
62103 while not reader .eof ():
63104 op = reader .read_bytes (1 )
64105 chunk = ScriptChunk (op )
65- data = None
66- if b'\x01 ' <= op <= b'\x4b ' :
67- data = reader .read_bytes (int .from_bytes (op , 'big' ))
68- elif op == OpCode .OP_PUSHDATA1 : # 0x4c
69- length = reader .read_uint8 ()
70- if length is not None :
71- data = reader .read_bytes (length )
72- elif op == OpCode .OP_PUSHDATA2 :
73- length = reader .read_uint16_le ()
74- if length is not None :
75- data = reader .read_bytes (length )
76- elif op == OpCode .OP_PUSHDATA4 :
77- length = reader .read_uint32_le ()
78- if length is not None :
79- data = reader .read_bytes (length )
106+
107+ in_conditional_block = self ._update_conditional_depth (
108+ op , in_conditional_block
109+ )
110+
111+ if op == OpCode .OP_RETURN and in_conditional_block == 0 :
112+ if self ._handle_op_return (reader , chunk ):
113+ break
114+ continue
115+
116+ data = self ._read_push_data (reader , op )
80117 chunk .data = data
81118 self .chunks .append (chunk )
82119
83120 def serialize (self ) -> bytes :
84- return self .script
121+ return self ._bytes
85122
86123 def hex (self ) -> str :
87- return self .script .hex ()
124+ return self ._bytes .hex ()
88125
89126 def byte_length (self ) -> int :
90- return len (self .script )
127+ return len (self ._bytes )
91128
92129 size = byte_length
93130
@@ -108,81 +145,91 @@ def is_push_only(self) -> bool:
108145
109146 def __eq__ (self , o : object ) -> bool :
110147 if isinstance (o , Script ):
111- return self .script == o .script
148+ return self ._bytes == o ._bytes
112149 return super ().__eq__ (o )
113150
114151 def __str__ (self ) -> str :
115- return self .script .hex ()
152+ return self ._bytes .hex ()
116153
117154 def __repr__ (self ) -> str :
118155 return self .__str__ ()
119156
120157 @classmethod
121- def from_chunks (cls , chunks : List [ScriptChunk ]) -> ' Script' :
122- script = b''
158+ def from_chunks (cls , chunks : List [ScriptChunk ]) -> " Script" :
159+ script = b""
123160 for chunk in chunks :
124- script += encode_pushdata (chunk .data ) if chunk .data is not None else chunk .op
161+ script += (
162+ encode_pushdata (chunk .data ) if chunk .data is not None else chunk .op
163+ )
125164 s = Script (script )
126165 s .chunks = chunks
127166 return s
128167
129168 @classmethod
130- def from_asm (cls , asm : str ) -> 'Script' :
169+ def _parse_opcode_token (cls , token : str ) -> Optional [bytes ]:
170+ """Parse a single token as an opcode. Returns opcode bytes or None."""
171+ if token in OPCODE_NAME_VALUE_DICT :
172+ return OPCODE_NAME_VALUE_DICT [token ]
173+ if token == "0" :
174+ return b"\x00 "
175+ if token == "-1" :
176+ return OpCode .OP_1NEGATE
177+ return None
178+
179+ @classmethod
180+ def _parse_data_token (cls , token : str ) -> tuple [bytes , bytes ]:
181+ """Parse a token as hex data. Returns (opcode, data) tuple."""
182+ hex_string = token
183+ if len (hex_string ) % 2 != 0 :
184+ hex_string = "0" + hex_string
185+ hex_bytes = bytes .fromhex (hex_string )
186+ if hex_bytes .hex () != hex_string .lower ():
187+ raise ValueError ("invalid hex string in script" )
188+
189+ hex_len = len (hex_bytes )
190+ if 0 <= hex_len < int .from_bytes (OpCode .OP_PUSHDATA1 , "big" ):
191+ opcode_value = int .to_bytes (hex_len , 1 , "big" )
192+ elif hex_len < pow (2 , 8 ):
193+ opcode_value = OpCode .OP_PUSHDATA1
194+ elif hex_len < pow (2 , 16 ):
195+ opcode_value = OpCode .OP_PUSHDATA2
196+ else :
197+ opcode_value = OpCode .OP_PUSHDATA4
198+ return (opcode_value , hex_bytes )
199+
200+ @classmethod
201+ def from_asm (cls , asm : str ) -> "Script" :
131202 chunks : [ScriptChunk ] = []
132- if not asm : # Handle empty string
203+ if not asm :
133204 return Script .from_chunks (chunks )
134- tokens = asm .split (' ' )
205+
206+ tokens = asm .split (" " )
135207 i = 0
136208 while i < len (tokens ):
137209 token = tokens [i ]
138- opcode_value : Optional [bytes ] = None
139- # BRC-106: Check if token is a recognized opcode (including aliases)
140- if token in OPCODE_NAME_VALUE_DICT :
141- opcode_value = OPCODE_NAME_VALUE_DICT [token ]
142- chunks .append (ScriptChunk (opcode_value ))
143- i += 1
144- elif token == '0' :
145- # Numeric literal 0
146- opcode_value = b'\x00 '
147- chunks .append (ScriptChunk (opcode_value ))
148- i += 1
149- elif token == '-1' :
150- # Numeric literal -1
151- opcode_value = OpCode .OP_1NEGATE
210+ opcode_value = cls ._parse_opcode_token (token )
211+
212+ if opcode_value is not None :
152213 chunks .append (ScriptChunk (opcode_value ))
153214 i += 1
154215 else :
155- # Assume it's hex data to push
156- hex_string = token
157- if len (hex_string ) % 2 != 0 :
158- hex_string = '0' + hex_string
159- hex_bytes = bytes .fromhex (hex_string )
160- if hex_bytes .hex () != hex_string .lower ():
161- raise ValueError ('invalid hex string in script' )
162- hex_len = len (hex_bytes )
163- if 0 <= hex_len < int .from_bytes (OpCode .OP_PUSHDATA1 , 'big' ):
164- opcode_value = int .to_bytes (hex_len , 1 , 'big' )
165- elif hex_len < pow (2 , 8 ):
166- opcode_value = OpCode .OP_PUSHDATA1
167- elif hex_len < pow (2 , 16 ):
168- opcode_value = OpCode .OP_PUSHDATA2
169- elif hex_len < pow (2 , 32 ):
170- opcode_value = OpCode .OP_PUSHDATA4
216+ opcode_value , hex_bytes = cls ._parse_data_token (token )
171217 chunks .append (ScriptChunk (opcode_value , hex_bytes ))
172- i = i + 1
218+ i += 1
219+
173220 return Script .from_chunks (chunks )
174221
175222 def to_asm (self ) -> str :
176- return ' ' .join (str (chunk ) for chunk in self .chunks )
223+ return " " .join (str (chunk ) for chunk in self .chunks )
177224
178225 @classmethod
179- def find_and_delete (cls , source : ' Script' , pattern : ' Script' ) -> ' Script' :
226+ def find_and_delete (cls , source : " Script" , pattern : " Script" ) -> " Script" :
180227 chunks = []
181228 for chunk in source .chunks :
182229 if Script .from_chunks ([chunk ]).hex () != pattern .hex ():
183230 chunks .append (chunk )
184231 return Script .from_chunks (chunks )
185232
186233 @classmethod
187- def write_bin (cls , octets : bytes ) -> ' Script' :
234+ def write_bin (cls , octets : bytes ) -> " Script" :
188235 return Script (encode_pushdata (octets ))
0 commit comments