Summary
embit accepts a brace-wrapped single-leaf Taproot tree such as:
tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,{pk(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)})
and silently canonicalizes it to:
tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,pk(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5))
Braces denote a two-child branch, so the single-child form is malformed and is rejected by the other maintained parsers I checked locally.
Minimal repro
from embit.descriptor.descriptor import Descriptor
K = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
K2 = "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
invalid = f"tr({K},{{pk({K2})}})" # brace-wrapped single leaf -> SHOULD be rejected
valid = f"tr({K},pk({K2}))" # plain single leaf -> valid
two_child = f"tr({K},{{pk({K2}),pk({K})}})" # real two-child branch -> valid (control)
inv = Descriptor.from_string(invalid) # bug: currently succeeds
val = Descriptor.from_string(valid)
two = Descriptor.from_string(two_child) # control: accepted, as expected
print(str(inv)) # -> tr(...,pk(...)) (braces silently dropped)
print(str(inv) == str(val)) # -> True
print(inv.script_pubkey().data.hex() == val.script_pubkey().data.hex()) # -> True
Observed behavior
On the current local checkout, Descriptor.from_string(invalid) succeeds, str(inv) becomes the canonical brace-less form, and the script matches valid exactly.
Both normalize to:
tr(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798,pk(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5))
and both compile to:
5120456b959d3ad02729d12d7df9a6ce66f2f02043fb5c5b61071897c59414a1842e
Cross-checks
The brace-wrapped single-child string is rejected by the other maintained parsers:
bitcoin/bitcoin: tr(): expected ',' after script expression
rust-miniscript: taptree branch must have 2 children, but found 1
bitcoinerlab/descriptors: invalid taproot tree expression: pk(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)
Controls, both accepted by embit, isolating the bug to single-child flattening rather than brace handling generally:
- Plain single leaf
tr(KEY,pk(KEY)) — accepted by embit and rust-miniscript
- Real two-child branch
tr(KEY,{pk(KEY),pk(KEY)}) — accepted by embit
Root cause
From local source review, TapTree.read_from() in src/embit/descriptor/taptree.py:104-114 treats {...} as a branch parser, but if it sees } immediately after parsing the left side, it returns the left child instead of rejecting the single-child branch:
if c == b"{": # line 104 — comment in source: "more than one miniscript"
left = cls.read_from(s)
c = s.read(1)
if c == b"}": # line 107
return left # line 108 — returns the single child instead of raising
if c != b",":
raise MiniscriptError("Invalid taptree syntax: expected ','") # line 110
right = cls.read_from(s)
if s.read(1) != b"}":
raise MiniscriptError("Invalid taptree syntax: expected '}'") # line 113
return cls((left, right)) # line 114
Two things show this is likely a missed case rather than intentional leniency:
- The branch handler's own comment says
{...} is for “more than one miniscript,” yet lines 107-108 accept exactly one.
- The parser raises
MiniscriptError in every other malformed case in this same block, such as a missing comma or missing closing brace. The single-child case is the one path that silently flattens {X} to X instead of raising.
Summary
embitaccepts a brace-wrapped single-leaf Taproot tree such as:and silently canonicalizes it to:
Braces denote a two-child branch, so the single-child form is malformed and is rejected by the other maintained parsers I checked locally.
Minimal repro
Observed behavior
On the current local checkout,
Descriptor.from_string(invalid)succeeds,str(inv)becomes the canonical brace-less form, and the script matchesvalidexactly.Both normalize to:
and both compile to:
Cross-checks
The brace-wrapped single-child string is rejected by the other maintained parsers:
bitcoin/bitcoin:tr(): expected ',' after script expressionrust-miniscript:taptree branch must have 2 children, but found 1bitcoinerlab/descriptors:invalid taproot tree expression: pk(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)Controls, both accepted by
embit, isolating the bug to single-child flattening rather than brace handling generally:tr(KEY,pk(KEY))— accepted byembitandrust-miniscripttr(KEY,{pk(KEY),pk(KEY)})— accepted byembitRoot cause
From local source review,
TapTree.read_from()insrc/embit/descriptor/taptree.py:104-114treats{...}as a branch parser, but if it sees}immediately after parsing the left side, it returns the left child instead of rejecting the single-child branch:Two things show this is likely a missed case rather than intentional leniency:
{...}is for “more than one miniscript,” yet lines 107-108 accept exactly one.MiniscriptErrorin every other malformed case in this same block, such as a missing comma or missing closing brace. The single-child case is the one path that silently flattens{X}toXinstead of raising.