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")
@jdlcdl
Copy link

jdlcdl commented Jan 13, 2025

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:

            "pk(key_1)",
            "or_b(pk(key_1),s:pk(key_2))",
            "or_d(pk(key_likely),pkh(key_unlikely))",
            "and_v(v:pk(key_user),or_d(pk(key_service),older(12960)))",
            "thresh(3,pk(key_1),pk(key_2),pk(key_3),older(12960))",
            "andor(pk(key_local),older(1008),pk(key_revocation))",
            "t:or_c(pk(key_revocation),and_v(v:pk(key_remote),or_c(pk(key_local),v:hash160(H))))",
            "andor(pk(key_remote),or_i(and_v(v:pkh(key_local),hash160(H)),older(1008)),pk(key_revocation))",

@jdlcdl
Copy link

jdlcdl commented Jan 13, 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)

@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