rust-bitcoin / rust-bitcoin/rust-miniscript

Satisfier: track non-removable signatures when assessing timelock malleability

Open
#1,048 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
426
Forks
200
Avg merge
7d 17h
Merged PRs (30d)
8

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read the satisfier logic in src/miniscript/satisfy/mod.rs and run the regression_1037 test, which covers both absolute and relative timelocks. Done means the reported cases return the expected witness through satisfy while the satisfy_malleable and explicit root_has_sig checks continue to demonstrate the expected behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
blockchain, cryptography
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.