Skip to content

Instantly share code, notes, and snippets.

@odudex
Last active January 14, 2025 17:23
Show Gist options
  • Save odudex/c2b54ac2488204619e3873cac8e4626b to your computer and use it in GitHub Desktop.
Save odudex/c2b54ac2488204619e3873cac8e4626b to your computer and use it in GitHub Desktop.
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")
@odudex
Copy link
Author

odudex commented Jan 14, 2025

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)

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