pyblock/pybitblock/oraclevision/script_parser.py
MarcanoFilms cc02310f1c Address Sourcery review: security hardening and config resilience
- Fix detect_token_patterns to scan decoded ASCII instead of hex strings
- Handle malformed oraclevision.conf without crashing the menu
- Add security.py to validate executables, paths, and RPC method names
- Resolve subprocess targets before launch with nosemgrep audit notes
2026-06-21 11:06:41 -04:00

239 lines
No EOL
6.9 KiB
Python

"""
Low-level Bitcoin script/witness parsing helpers.
Ported from OracleVision (https://github.com/MarcanoFilms/oraculovision).
These functions implement BIP-110 size checks and spam heuristics. Extend
this module when adding new detection rules — keep UI code separate.
"""
from __future__ import annotations
import re
from typing import Iterable
# BIP-110 size limits (reduced_data policy)
MAX_SCRIPTPUBKEY_SIZE = 34
MAX_OPRETURN_SIZE = 83
MAX_PUSHDATA_SIZE = 256
MAX_CONTROL_BLOCK_SIZE = 257
OP_IF = 0x63
OP_NOTIF = 0x64
OP_FALSE = 0x00
_SPAM_HEX_PATTERNS = (
b"6272632d3230", # brc-20
b"2270223a22627263", # "p":"brc
b"7469636b", # tick
b"6f7264", # ord
b"52554e45", # RUNE
b"746578742f706c61696e", # text/plain
)
def hex_to_bytes(hex_str: str) -> bytes:
if not hex_str:
return b""
try:
return bytes.fromhex(hex_str)
except ValueError:
return b""
def witness_total_bytes(witness: Iterable[str] | None) -> int:
if not witness:
return 0
return sum(len(w) // 2 for w in witness)
def has_annex(witness: list[str] | None) -> bool:
if not witness or len(witness) < 2:
return False
return witness[-1].startswith("50")
def is_valid_taproot_control_block(cb_hex: str) -> bool:
cb = hex_to_bytes(cb_hex)
if len(cb) < 33 or (len(cb) - 33) % 32 != 0:
return False
return (cb[0] & 0xFE) >= 0xC0
def infer_taproot_script_path(witness: list[str]) -> bool:
"""Infer taproot script-path spend from witness structure without prevout."""
if not witness or len(witness) < 3:
return False
annex = has_annex(witness)
cb_idx = len(witness) - (2 if annex else 1)
if cb_idx < 1:
return False
return is_valid_taproot_control_block(witness[cb_idx])
def script_has_large_push(script_hex: str) -> bool:
"""BIP-110 Rule 2: OP_PUSHDATA payloads > 256 bytes inside executing scripts."""
buf = hex_to_bytes(script_hex)
i = 0
while i < len(buf):
op = buf[i]
if 0x01 <= op <= 0x4B:
header_len, data_len = 1, op
elif op == 0x4C:
if i + 2 > len(buf):
break
header_len, data_len = 2, buf[i + 1]
elif op == 0x4D:
if i + 3 > len(buf):
break
header_len, data_len = 3, int.from_bytes(buf[i + 1 : i + 3], "little")
elif op == 0x4E:
if i + 5 > len(buf):
break
header_len, data_len = 5, int.from_bytes(buf[i + 1 : i + 5], "little")
else:
i += 1
continue
if i + header_len + data_len > len(buf):
break
if data_len > MAX_PUSHDATA_SIZE:
return True
i += header_len + data_len
return False
def scan_tapscript_violations(script_hex: str) -> tuple[bool, bool]:
"""Return (op_success, op_if_notif) for BIP-110 rules 6 & 7."""
buf = hex_to_bytes(script_hex)
op_success = False
op_if_notif = False
i = 0
while i < len(buf):
op = buf[i]
if 0x01 <= op <= 0x4B:
i += 1 + op
continue
if op == 0x4C:
if i + 1 >= len(buf):
break
i += 2 + buf[i + 1]
continue
if op == 0x4D:
if i + 2 >= len(buf):
break
i += 3 + int.from_bytes(buf[i + 1 : i + 3], "little")
continue
if op == 0x4E:
if i + 4 >= len(buf):
break
i += 5 + int.from_bytes(buf[i + 1 : i + 5], "little")
continue
if (
op in (80, 98)
or (126 <= op <= 129)
or (131 <= op <= 134)
or (137 <= op <= 138)
or (141 <= op <= 142)
or (149 <= op <= 153)
or (187 <= op <= 254)
):
op_success = True
elif op in (OP_IF, OP_NOTIF):
op_if_notif = True
i += 1
if op_success and op_if_notif:
break
return op_success, op_if_notif
def has_op_false_op_if_envelope(script_hex: str) -> bool:
"""Detect Ordinals inscription envelope: OP_FALSE ... OP_IF."""
buf = hex_to_bytes(script_hex)
if len(buf) < 3:
return False
for i in range(len(buf) - 1):
if buf[i] == OP_FALSE and buf[i + 1] == OP_IF:
return True
return False
def detect_inscription_in_witness(witness: list[str] | None) -> bool:
if not witness:
return False
annex = has_annex(witness)
if len(witness) > (2 if annex else 1):
script_idx = len(witness) - (3 if annex else 2)
if script_idx >= 0:
tapscript = witness[script_idx]
if has_op_false_op_if_envelope(tapscript):
return True
for item in witness:
if has_op_false_op_if_envelope(item):
return True
return False
def detect_token_patterns(hex_blob: str) -> set[str]:
"""Heuristic detection of BRC-20, Runes, Ordinals content in hex."""
found: set[str] = set()
raw = hex_to_bytes(hex_blob)
ascii_text = raw.decode("ascii", errors="ignore").lower()
if b"6272632d3230" in raw or b'"p":"brc-20"' in raw or b'"p": "brc-20"' in raw:
found.add("brc20")
if b"7469636b" in raw and (b"627263" in raw or b"6f7264" in raw):
found.add("brc20")
if b"52554e45" in raw:
found.add("runes")
if b"6f7264" in raw or b"746578742f706c61696e" in raw:
found.add("ordinals")
if "ord" in ascii_text or "inscription" in ascii_text:
found.add("ordinals")
for pat in _SPAM_HEX_PATTERNS:
if pat in raw:
if pat == b"52554e45":
found.add("runes")
elif pat in (b"6272632d3230", b"2270223a22627263", b"7469636b"):
found.add("brc20")
else:
found.add("ordinals")
return found
def decode_coinbase_tag(coinbase_hex: str) -> str:
"""Extract readable miner/pool tag from coinbase hex."""
raw = hex_to_bytes(coinbase_hex)
if len(raw) < 4:
return "unknown"
text = raw.decode("ascii", errors="ignore")
runs = re.findall(r"[\x20-\x7e]{4,}", text)
if not runs:
return "unknown"
pool_runs = [r.strip() for r in runs if "/" in r and len(r.strip()) >= 5]
if pool_runs:
return max(pool_runs, key=len)[:40]
candidates = [r.strip() for r in runs if len(r.strip()) >= 6]
if candidates:
return max(candidates, key=len)[:40]
return runs[-1].strip()[:40] or "unknown"
def is_signaling_bip110(version: int) -> bool:
"""BIP-110 reduced_data uses version bit 4."""
return bool(version & (1 << 4))
def vout_script_size(vout: dict) -> int:
spk = vout.get("scriptPubKey", {})
hex_data = spk.get("hex", "")
return len(hex_data) // 2
def is_op_return(vout: dict) -> bool:
spk = vout.get("scriptPubKey", {})
return spk.get("type") == "nulldata" or spk.get("asm", "").startswith("OP_RETURN")