Last active
January 14, 2025 17:23
-
-
Save odudex/c2b54ac2488204619e3873cac8e4626b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Node: | |
""" | |
A simple parse-tree node to represent Miniscript expressions. | |
""" | |
def __init__(self, node_type, children=None, value=None, level=0): | |
self.node_type = node_type # e.g. "AND", "OR", "PK", "OLDER" | |
self.children = children if children is not None else [] | |
self.value = value # used by PK(...) or OLDER(...) | |
self.level = level # for indentation | |
def strip_wrappers(expr): | |
""" | |
Miniscript often has wrappers like 'v:', 'c:', etc. | |
We manually remove them if present. | |
""" | |
wrapper_count = 0 | |
while True: | |
if expr[wrapper_count] in "asctdvjnlu": | |
wrapper_count += 1 | |
elif expr[wrapper_count] == ":": | |
return expr[wrapper_count + 1 :] | |
else: | |
break | |
return expr | |
def parse_operator(expr): | |
""" | |
A small helper to detect if expr is of the form op(...). | |
Returns (op, inside) if found, or (None, None) if not. | |
""" | |
expr = expr.strip() | |
# find the first '(' | |
pos = expr.find("(") | |
if pos == -1: | |
return None, None | |
# check if the expr ends with ')' | |
if not expr.endswith(")"): | |
return None, None | |
op = expr[:pos].strip() | |
inside = expr[pos + 1 : -1].strip() # content inside the outer parentheses | |
return op, inside | |
def parse_miniscript(expr, level=0): | |
""" | |
Recursively parse a (limited) Miniscript expression into a Node tree. | |
""" | |
expr = strip_wrappers(expr.strip()) # remove wrappers (e.g. 'v:') if present | |
# Expressions with no children | |
end_expressions = { | |
"pk": "PK", | |
"pkh": "PKH", | |
"older": "OLDER", | |
"after": "AFTER", | |
"multi": "MULTI", | |
"multi_a": "MULTI", | |
} | |
# Check for expressions that don't have children | |
for prefix, node_type in end_expressions.items(): | |
if expr.startswith(prefix + "(") and expr.endswith(")"): | |
value = expr[len(prefix) + 1 : -1].strip() | |
return Node( | |
node_type, | |
value=( | |
value | |
if node_type != "OLDER" and node_type != "AFTER" | |
else int(value) | |
), | |
level=level, | |
) | |
# Otherwise, check if it's an operator call: op(...) | |
op, inside = parse_operator(expr) | |
if op is not None: | |
# We'll need to split the top-level arguments X,Y inside the parentheses. | |
# But we have to be careful with nested parentheses. | |
args = split_top_level_args(inside, ",") | |
if op == "thresh": | |
n = int(args[0]) | |
children = [parse_miniscript(a, level=level + 1) for a in args[1:]] | |
return Node("THRESH", children=children, value=n, level=level) | |
node_types = { | |
"and_v": "AND", | |
"and_b": "AND", | |
"or_c": "OR", | |
"or_d": "OR", | |
"andor": "ANDOR", | |
"or_i": "OR_I", | |
} | |
if op in node_types: | |
children = [parse_miniscript(a, level=level + 1) for a in args] | |
return Node(node_types[op], children=children, level=level) | |
# Add more operator handling here as needed | |
raise ValueError("Unrecognized miniscript pattern: {}".format(expr)) | |
def split_top_level_args(s, delimiter): | |
""" | |
Splits a string `s` by the top-level delimiter (commas in "X,Y"), | |
respecting parentheses so we don't split inside nested calls. | |
Example: | |
inside = "pk(B),or_c(pk(C),v:older(1000))" | |
=> ["pk(B)", "or_c(pk(C),v:older(1000))"] | |
""" | |
parts = [] | |
bracket_level = 0 | |
current = [] | |
for ch in s: | |
if ch == "(": | |
bracket_level += 1 | |
current.append(ch) | |
elif ch == ")": | |
bracket_level -= 1 | |
current.append(ch) | |
elif ch == delimiter and bracket_level == 0: | |
# top-level comma found | |
parts.append("".join(current).strip()) | |
current = [] | |
else: | |
current.append(ch) | |
# push the last part | |
if current: | |
parts.append("".join(current).strip()) | |
return parts | |
def node_to_policy(node): | |
""" | |
Convert a parsed Node tree into a user-friendly 'policy' indented text. | |
""" | |
def newline_indent(a_string, level=0): | |
return "\n" + " " * level + a_string | |
node_string = "" | |
t = node.node_type | |
if t in ["PK", "PKH", "OLDER", "AFTER", "MULTI"]: | |
node_string = "{}({})".format(t.lower(), node.value) | |
elif t in ["AND", "OR", "ANDOR", "OR_I"]: | |
children_pol = [node_to_policy(c) for c in node.children] | |
node_string = "{}({})".format(t.lower(), ",".join(children_pol)) | |
elif t == "THRESH": | |
children_pol = [node_to_policy(c) for c in node.children] | |
node_string = "thresh({},{})".format(node.value, ",".join(children_pol)) | |
else: | |
raise ValueError("Unknown node type: {}".format(t)) | |
return newline_indent(node_string, node.level) | |
# Example usage | |
if __name__ == "__main__": | |
miniscripts = [ | |
"or_d(pk(A),and_v(v:pkh(B),older(6)))", | |
"and_v(or_c(pk(B),or_c(pk(C),v:older(1000))),pk(A))", | |
"or_d(multi(2,A,B),and_v(v:thresh(2,pkh(C),a:pkh(D),a:pkh(E)),older(144)))", | |
"andor(multi(2,A,B,C),or_i(and_v(v:pkh(D),after(230436)),thresh(2,pk(E),s:pk(F),s:pk(G),snl:after(230220))),and_v(v:thresh(2,pkh(H),a:pkh(I),a:pkh(J)),after(230775)))", | |
] | |
for miniscript in miniscripts: | |
print("Miniscript:", miniscript) | |
# 1) Parse into a Node tree | |
tree = parse_miniscript(miniscript) | |
# 3) Convert to policy | |
policy_str_simplified = node_to_policy(tree) | |
print("Policy:", policy_str_simplified, "\n") |
On indentation:
If Node.__init__()
also accepts level=0
,
and
parse_miniscript()
accepts level=-1
and creates new nodes like level=level+1
and same when calling itself recursively
then node_to_policy()
can have a sub function like:
def newline_indent(a_string, level):
return "\n" + " "*level + a_string)
On indentation:
If
Node.__init__()
also acceptslevel=0
,and
parse_miniscript()
acceptslevel=-1
and creates new nodes likelevel=level+1
and same when calling itself recursivelythen
node_to_policy()
can have a sub function like:def newline_indent(a_string, level): return "\n" + " "*level + a_string)
Updated to add indentation. Thank you!
I'll later test/adapt with Sipa's references.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
At the Miniscript reference page: https://bitcoin.sipa.be/miniscript/
There are some examples that when clicked, auto-fill the Source Policy form and submit it, as well as filling the Analzye Miniscript form and submit it too. So might shoot to support similar analysis and Miniscript to Policy decompilation for these examples: