Last active
January 15, 2025 13:10
-
-
Save odudex/36e1fc57d8d8ea3acabf654de43fe320 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
END_OPERATORS = [ | |
"pk_k", | |
"pk_h", | |
"older", | |
"after", | |
"sha256", | |
"hash256", | |
"ripemd160", | |
"hash160", | |
"multi", | |
"sortedmulti", | |
"multi_a", | |
"sortedmulti_a", | |
"pk", | |
"pkh", | |
] | |
OPERATORS = [ | |
"andor", | |
"and_v", | |
"and_b", | |
"and_n", | |
"or_b", | |
"or_c", | |
"or_d", | |
"or_i", | |
"thresh", | |
] | |
INTEGER_NODE = "int" | |
class Node: | |
""" | |
A simple parse-tree node to represent Miniscript expressions. | |
""" | |
def __init__(self, node_type, children=None, value=None, properties=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.properties = properties # e.g. "v:", "c:", etc. | |
self.level = level # for indentation | |
def strip_properties(expr): | |
""" | |
Miniscript often has property wrappers like 'v:', 'c:', etc. | |
We manually isolate them if present. | |
""" | |
properties_count = 0 | |
while True: | |
if expr[properties_count] in "asctdvjnlu": | |
properties_count += 1 | |
elif expr[properties_count] == ":": | |
return expr[properties_count + 1 :], expr[: properties_count + 1] | |
else: | |
break | |
return expr, None | |
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, properties = strip_properties( | |
expr.strip() | |
) # detect and isolate wrappers (e.g. 'v:') if present | |
# Check for expressions that don't have children | |
for operator in END_OPERATORS: | |
if expr.startswith(operator + "(") and expr.endswith(")"): | |
value = expr[len(operator) + 1 : -1].strip() | |
return Node( | |
operator, | |
value=( | |
value if operator not in ("int", "older", "after") else int(value) | |
), | |
properties=properties, | |
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 = [Node(INTEGER_NODE, value=args[0], level=level + 1)] | |
children += [parse_miniscript(a, level=level + 1) for a in args[1:]] | |
return Node( | |
"thresh", children=children, value=n, properties=properties, level=level | |
) | |
if op in OPERATORS: | |
children = [parse_miniscript(a, level=level + 1) for a in args] | |
return Node(op, children=children, properties=properties, 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 == INTEGER_NODE: | |
node_string = node.value | |
elif t in END_OPERATORS: | |
node_string = "{}({})".format(t, node.value) | |
elif t in OPERATORS: | |
children_pol = [node_to_policy(c) for c in node.children] | |
node_string = "{}({})".format(t, ",".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)) | |
if node.properties: | |
node_string = node.properties + node_string | |
return newline_indent(node_string, node.level) | |
# Example usage | |
if __name__ == "__main__": | |
miniscripts = [ | |
# Sipa's example scripts | |
"pk(A)", | |
"or_b(pk(A),s:pk(B))", | |
"or_d(pk(A),pkh(B))", | |
"and_v(v:pk(A),or_d(pk(B),older(12960)))", | |
"thresh(3,pk(A),pk(B),pk(C),older(12960))", | |
"andor(pk(A),older(1008),pk(B))", | |
"t:or_c(pk(A),and_v(v:pk(B),or_c(pk(C),v:hash160(e7d285b4817f83f724cd29394da75dfc84fe639e))))", | |
"andor(pk(A),or_i(and_v(v:pkh(B),hash160(e7d285b4817f83f724cd29394da75dfc84fe639e)),older(1008)),pk(C))", | |
# Other examples | |
"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("Indented View:", policy_str_simplified, "\n") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment