Skip to content

Fix: ct() taproot descriptors with script tree don't parse - #109

Closed
KyrylR wants to merge 2 commits into
ElementsProject:masterfrom
KyrylR:fix/ct-tr-script-tree
Closed

KyrylR wants to merge 2 commits into
ElementsProject:masterfrom
KyrylR:fix/ct-tr-script-tree

Conversation

@KyrylR

@KyrylR KyrylR commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes: #105

Overall with this PR the #105 will be fixed, there are separate issues mentioned in the end that are worth checking

In comparison to before the code became simpler

Rational (created with a help of GPT Astra)

Consider:

eltr(K,pk(A))

The new new_count == 0 check here: https://github.com/KyrylR/elements-miniscript/blob/fa1725eea6d03a45735440da0c46ef8944f13598/src/expression.rs#L117 separates the first closing parenthesis in pk(A) and the next closing eltr.

“Brace mode” also works for a single leaf without braces. It preserves the leaf’s parentheses as part of its text while looking for tree separators.

In https://github.com/KyrylR/elements-miniscript/blob/fa1725eea6d03a45735440da0c46ef8944f13598/src/descriptor/tr.rs#L693, the standalone parser becomes:

let desc_str = verify_checksum(s)?;
let top = expression::Tree::from_str(desc_str)?;
Self::from_tree(&top)

Line by line:

  1. Validate the checksum if one is present, then obtain the descriptor body.
  2. Parse that body using the shared expression parser.
  3. Convert the expression tree into a typed Taproot descriptor.

That makes the old parse_tr_tree and its custom split_once unnecessary. Their responsibilities move to shared parsing: recognizing arguments, checking delimiters, rejecting leftover input, and constructing the expression tree. The removed imports were only needed by those helpers.

Note

One subtle behavior change deserves care: the old parser could accept eltr(A,) with permissive String keys by treating "A," as the key. Real public-key types already rejected that text. The new rejection removes that generic-key quirk.

In https://github.com/KyrylR/elements-miniscript/blob/fa1725eea6d03a45735440da0c46ef8944f13598/src/descriptor/mod.rs#L1191, the second commit moves extension selection into from_tree:

match Tr::<Pk, NoExt>::from_tree(top) {
    Ok(tr) => Descriptor::Tr(tr),
    Err(_) => Descriptor::TrExt(Tr::<Pk, T>::from_tree(top)?),
}
  • Tr::<Pk, NoExt> first tries ordinary Miniscript.
  • Ok(tr) preserves the normal Descriptor::Tr variant.
  • Err(_) triggers another attempt using extension type T.
  • Successful extension parsing produces Descriptor::TrExt.
  • The final ? returns an error if that attempt also fails.

This fallback previously lived only in Descriptor::from_str. Moving it into from_tree means confidential wrappers reach it too.

Then https://github.com/KyrylR/elements-miniscript/blob/fa1725eea6d03a45735440da0c46ef8944f13598/src/descriptor/mod.rs#L1214-L1216 give ordinary descriptors one parsing sequence: checksum, expression tree, typed descriptor. The special eltr string-prefix branch disappears.

There are two follow-up issues that probably should be addressed separately from the parser changes.

  1. Validate confidential descriptors before ELIP151 derivation. The confidential parser enters through from_tree, bypassing the standalone multipath-length check. ELIP151 code then assumes expansion succeeds using expect. Invalid multipath combinations can therefore panic instead of returning an error. The defensive fix is to validate the inner descriptor before deriving the view key and propagate expansion errors.

  2. Reject unsupported Taproot pegin descriptors before use. Pegin parsing accepts ordinary descriptors through the shared converter, but later consumers call explicit_script().expect(...), including dynafed pegin and legacy pegin. Taproot deliberately returns TrNoScriptCode from that method. Reject unsupported Tr and TrExt descriptors at the pegin boundary, or make the consumers return an appropriate error.

Both problems predate this branch. The new parser makes additional tree forms reach them.

@KyrylR
KyrylR marked this pull request as ready for review September 30, 2026 10:49
@KyrylR KyrylR changed the title Fix/ct tr script tree Fix: ct() taproot descriptors with script tree don't parse Sep 30, 2026
@KyrylR
KyrylR marked this pull request as draft September 30, 2026 10:55
@KyrylR

KyrylR commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

With this PR I learned more about parsing descriptors, I believe only tests from this PR are relevant due to the proposed solution here: #108 (comment)

Closing this PR in favor of first PR that will follow steps proposed here: #108 (comment)

@KyrylR KyrylR closed this Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ct() taproot descriptors with script tree don't parse

1 participant