Skip to content

Commit 4c30e2e

Browse files
authored
V1.0.11p (#138)
* Patching issue: #135 * Removing file * Addressed Qube issues * Committing test * SONAR
1 parent f505ea5 commit 4c30e2e

File tree

3 files changed

+335
-283
lines changed

3 files changed

+335
-283
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ htmlcov/
99
.coverage
1010
build/
1111
.venv/
12-
.env.local
12+
.env*
1313
.wallet

bsv/script/script.py

Lines changed: 116 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66
# BRC-106 compliance: Opcode aliases for parsing
77
# Build a comprehensive mapping of all opcode names (including aliases) to their byte values
88
OPCODE_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
1616
OPCODE_NAME_VALUE_DICT = {item.name: item.value for item in OpCode}
1717
# Merge with aliases
1818
OPCODE_NAME_VALUE_DICT.update(OPCODE_ALIASES)
1919

20+
2021
class 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

Comments
 (0)