Skip to content

Satisfier: track non-removable signatures when assessing timelock malleability #1048

Description

@guggero

Requested by this comment.
PR #1037 introduces a new regression_1037 test that demonstrates this in src/miniscript/satisfy/mod.rs.

The following text is from GPT-6.

Problem

The satisfier passes a single root_has_sig boolean down to every timelock
fragment. This is intended to indicate that every satisfaction requires a
signature. When a timelock check fails:

  • With root_has_sig, the branch is Witness::Impossible: the signature
    constrains the transaction's timelock fields, so a third party cannot change
    them to make the branch available.
  • Without it, the branch is Witness::Unavailable: the satisfier cannot use
    it, but a third party might be able to change the transaction's timelocks.

This is a coarse approximation of whether the witness being constructed
contains a signature that a third party cannot remove. It can reject a
non-malleable witness because a timelocked alternative is treated as a
possible malleation.

The malleability enum makes this approximation more conservative: expressions
classified as Malleability::Malleable no longer retain a signature property,
even when every spend requires a signature. Consequently,
non_malleable_and_requires_sig() returns false for those expressions.

Reproduction

Consider the following Miniscript, parsed with from_str_insane:

and_v(v:pk(K),or_i(after(100),after(200)))

Provide a signature for K and an absolute height locktime of 100. Every
spend requires that signature. Only the first timelock is satisfied, and a
third party cannot change the transaction's locktime to select the other
branch without invalidating the signature.

Expected: satisfy returns the first branch's witness, [1, sig(K)].

Actual: the root is classified as malleable, so root_has_sig is false.
The unmet after(200) becomes Unavailable, and the two unsigned or_i
alternatives cause satisfy to return Error::CouldNotSatisfy.

The same problem occurs with
and_v(v:pk(K),or_i(older(100),older(200))) and a relative height locktime
of 100.

regression_1037 in src/miniscript/satisfy/mod.rs covers both cases. It
also demonstrates that satisfy_malleable returns the expected witness and
that explicitly passing root_has_sig = true to the internal satisfier
recovers that witness through the non-malleable algorithm.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions