Skip to content

compile_tr* panics on recoverable Taproot-construction errors instead of returning normal errors #995

Description

@Nuhiat-Arefin

Summary

rust-miniscript's Taproot policy compiler currently converts at least two recoverable Taproot-construction failures into panics via internal expect(...) calls instead of surfacing normal errors.

Verified locally on commit 6131095c8ee99ba9a91ee004b1d387dd8418766a.

Confirmed variants locally:

1. Uncompressed chosen/internal key

Affected entrypoints:

  • Policy::<bitcoin::PublicKey>::compile_tr(...)
  • Policy::<bitcoin::PublicKey>::compile_tr_native(...)
  • Policy::<bitcoin::PublicKey>::compile_to_descriptor(DescriptorCtx::Tr(..))

These panic with:

compiler produces sane output: ContextError(UncompressedKeysNotAllowed)

By contrast:

  • Policy::<bitcoin::PublicKey>::compile_tr_private_experimental(...)

returns:

Err(ContextError(UncompressedKeysNotAllowed))

2. Overdeep Huffman TapTree from a skewed root-level disjunction

Affected entrypoints:

  • Policy::compile_tr(...)
  • Policy::compile_tr_native(...)
  • Policy::compile_tr_private_experimental(...)
  • Policy::compile_to_descriptor::<Tap>(DescriptorCtx::Tr(..))

These panic with:

huffman tree cannot produce depth > 128 given sane weights: TapTreeDepthError

Representative repro 1: uncompressed chosen internal key

use std::panic::AssertUnwindSafe;
use std::str::FromStr;

use miniscript::bitcoin::PublicKey;
use miniscript::policy::concrete::Policy;

fn main() {
    let uncompressed = PublicKey::from_str(
        "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8",
    )
    .unwrap();

    let policy = Policy::<PublicKey>::Trivial;

    let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
        policy.compile_tr(Some(uncompressed))
    }));

    println!("{res:?}");
}

Observed locally:

  • Policy::<PublicKey>::Trivial.compile_tr(Some(uncompressed)) panics with:
compiler produces sane output: ContextError(UncompressedKeysNotAllowed)
  • compile_tr_native(Some(uncompressed), ...) panics with the same error.
  • compile_to_descriptor(DescriptorCtx::Tr(Some(uncompressed))) panics with the same error.
  • The same payload passed to compile_tr_private_experimental(...) returns:
Err(ContextError(UncompressedKeysNotAllowed))

I also rechecked a useful control locally: a two-branch policy containing one uncompressed key and one compressed key compiled successfully through compile_tr(...) and compile_tr_native(...).

That narrows this crash to cases where the chosen internal key itself is uncompressed, rather than any appearance of an uncompressed key anywhere in the policy.

Representative repro 2: overdeep Huffman TapTree

This public manual policy shape is enough:

  • Build a right-comb of Policy::Or
  • Use weights (1, 1) at every Or
  • Use unique pk(Ki) leaves
use std::sync::Arc;

use miniscript::policy::concrete::Policy;

fn main() {
    let mut leaves = Vec::new();
    for i in 0..131 {
        leaves.push(Policy::Key(format!("K{i}")));
    }

    let policy = build_weighted_or(&leaves);
    let _ = policy.compile_tr(None::<String>);
}

fn build_weighted_or(leaves: &[Policy<String>]) -> Policy<String> {
    if leaves.len() == 2 {
        return Policy::Or(vec![
            (1, Arc::new(leaves[0].clone())),
            (1, Arc::new(leaves[1].clone())),
        ]);
    }

    Policy::Or(vec![
        (1, Arc::new(leaves[0].clone())),
        (1, Arc::new(build_weighted_or(&leaves[1..]))),
    ])
}

Observed locally:

  • 130 leaves: succeeds
  • 131 leaves: panics with:
huffman tree cannot produce depth > 128 given sane weights: TapTreeDepthError

I also rechecked this locally with tiny standalone repro binaries on both entry paths:

  • Manual Policy::Or construction
  • Parsed textual concrete policy

Both showed the same threshold, so this is reachable from parsed textual concrete policy as well as manual enum construction.

Why this seems to happen

There are two separate expect(...) sites turning recoverable Taproot-construction failures into panics.

1. Descriptor::new_tr(...) error is unwrapped

Public compile_tr(...) / compile_tr_native(...) call Descriptor::new_tr(...) and unwrap it with:

expect("compiler produces sane output")

Descriptor::new_tr(...) itself returns Result<_, Error>.

When the chosen or caller-supplied internal key is uncompressed, the recoverable error:

ContextError(UncompressedKeysNotAllowed)

is converted into a panic there.

By contrast, compile_tr_private_experimental(...) uses:

Descriptor::new_tr(...)?

which is why the same input returns a normal error on that entrypoint.

2. TapTree::combine(...) error is unwrapped

The shared with_huffman_tree(...) helper does:

TapTree::combine(...)
    .expect("huffman tree cannot produce depth > 128 given sane weights")

But TapTree::combine(...) itself is defined as:

pub fn combine(left: Self, right: Self) -> Result<Self, TapTreeDepthError>

So once a skewed Huffman construction would exceed depth 128, that recoverable TapTreeDepthError is also converted into a panic.

One source-level asymmetry appears to be that CompilerError currently has no variant carrying either ContextError or TapTreeDepthError, which helps explain why the public compile_tr(...) / compile_tr_native(...) path currently uses expect(...) instead of error propagation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions