Skip to content

Instantly share code, notes, and snippets.

@odudex
Last active January 15, 2025 13:10
Show Gist options
  • Save odudex/36e1fc57d8d8ea3acabf654de43fe320 to your computer and use it in GitHub Desktop.
Save odudex/36e1fc57d8d8ea3acabf654de43fe320 to your computer and use it in GitHub Desktop.
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