Skip to content

Conversation

@evanlinjin
Copy link
Contributor

Fix Plan::satisfy() to correctly append the witnessScript as the final witness stack element for P2WSH descriptor types (Wsh, WshSortedMulti, ShWsh, ShWshSortedMulti).

Previously, these types were incorrectly grouped with Wpkh and Tr, which don't require a trailing witness script. This caused transactions built using Plan::satisfy() to fail validation with "Witness program hash mismatch" when broadcast.

The fix separates the descriptor type handling:

  • Wpkh/Tr: return stack as-is (no witness script needed)
  • Wsh/WshSortedMulti: append witness script, empty script_sig
  • ShWpkh: return stack with unsigned_script_sig (no witness script)
  • ShWsh/ShWshSortedMulti: append witness script and unsigned_script_sig

Closes #896

🤖 Generated with Claude Code

@evanlinjin evanlinjin force-pushed the fix/plan-satisfy-does-not-append-witness-script-for-p2wsh branch from 66692de to 962bcc2 Compare February 3, 2026 05:43
@evanlinjin
Copy link
Contributor Author

The other option is to update Plan::satisfy docs to mention that witnessScript will not be pushed to the witness stack. However, I don't think that makes any sense.

@evanlinjin
Copy link
Contributor Author

cc @tcharding

@apoelstra
Copy link
Member

In 962bcc2:

It looks like you have two identical branches that only differ in their expect text. You should just combine these.

Thanks for the LLM disclosure! And nice job finding this bug.

@evanlinjin evanlinjin force-pushed the fix/plan-satisfy-does-not-append-witness-script-for-p2wsh branch from 34a5f89 to 8d9a61a Compare February 4, 2026 01:43
use miniscript::{
bitcoin, hash256, Descriptor, DescriptorPublicKey, Error, Miniscript, ScriptContext,
Translator,
bitcoin, hash256, Descriptor, DescriptorPublicKey, Error, Miniscript, ScriptContext, Translator,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If you feel like it can you throw a patch at the front of this PR that runs the formatter in bitcoind-tests please mate so that these fromatting changes aren't mixed in with your changes.

Comment on lines 428 to 429
/// Test that Plan::satisfy() correctly constructs witness for P2WSH descriptors.
/// This is a regression test for https://github.com/rust-bitcoin/rust-miniscript/issues/896
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rekon we can trim a bit more LLM noise out of this patch. I see no value in these comments.

assert!(num_conf > 0);

Ok(unsigned_tx.input[0].witness.clone())
}
Copy link
Member

@tcharding tcharding Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repo is not my baby so defering to @apoelstra.

Do we want to have code like this in the repo? It may do whats needed but its pretty far from clean code.

(Not a dig at you @evanlinjin more a philosophical / high level comment on do we want to merge LLM generated test code since users will look at it and copy it and think we wrote it.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of this is to test what we have against Bitcoin core - can we broadcast a tx with wsh spend(s)?

In terms of "cleanliness", I would argue it's quite easy to read as it is? Could you expand on this please, thanks.

println!("Testing wsh(multi(2,K1,K2,K3)) with Plan::satisfy()");
test_plan_satisfy_wsh(cl, &testdata, "wsh(multi(2,K1,K2,K3))").unwrap();

// Test P2SH-wrapped P2WSH with Plan::satisfy()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to see two tests instead of these separator style comments.

println!("Testing sh(wsh(pk(K))) with Plan::satisfy()");
test_plan_satisfy_wsh(cl, &testdata, "sh(wsh(pk(K)))").unwrap();

println!("Testing sh(wsh(multi(2,K1,K2,K3))) with Plan::satisfy()");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these println statements are unneeded IMO.

Comment on lines 300 to 308
| DescriptorType::ShWshSortedMulti => {
let mut stack = stack;
let witness_script = self
.descriptor
.explicit_script()
.expect("wsh descriptors have explicit script");
stack.push(witness_script.into_bytes());
let script_sig = self.descriptor.unsigned_script_sig();
(stack, script_sig)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| DescriptorType::ShWshSortedMulti => {
let mut stack = stack;
let witness_script = self
.descriptor
.explicit_script()
.expect("wsh descriptors have explicit script");
stack.push(witness_script.into_bytes());
let script_sig = self.descriptor.unsigned_script_sig();
(stack, script_sig)
}
| DescriptorType::ShWshSortedMulti => {
let mut stack = stack;
// We need to append the witness script as per BIP-141
let witness_script = self
.descriptor
.explicit_script()
.expect("wsh descriptors have explicit script");
stack.push(witness_script.into_bytes());
(stack, self.descriptor.unsigned_script_sig())
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't hack on this repo much so am not super familiar with the style but I'd write it like that. Specifically making the return statement mirror the other one that is the same while highlighting the difference.

Copy link
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @evanlinjin for the bug find and the fix. I appreciate that you have other work to do so I'm hesitant to ask too much of you but from my personal view point this PR has way too much test code for this fix. The bare minimum test code to prove that the bug exits would be better IMO.

EDIT: hmm, re-reading my review I see I suggested cleaning up the test code then said "there is too much test code" ... I think you will get where I'm coming from.

@evanlinjin evanlinjin force-pushed the fix/plan-satisfy-does-not-append-witness-script-for-p2wsh branch 4 times, most recently from 1c841fa to 026943a Compare February 4, 2026 09:34
@evanlinjin
Copy link
Contributor Author

evanlinjin commented Feb 4, 2026

@tcharding I've cleaned things up as much as I can.

I think the integration test is worth keeping as it checks the logic against Bitcoin Core - however, I'm happy to remove them as well.

@tcharding
Copy link
Member

tcharding commented Feb 4, 2026

Thanks @evanlinjin. I did a more thorough read of the test code. Its not perfect but I retract my statement that there is just too much test code. Thanks for iterating.

My review doesn't hold too much weight in this repo but FWIW this LGTM.

One nit: can we format with the nightly toolchain like we do for the rest of the repo?

Reviewed: 026943a

evanlinjin and others added 3 commits February 9, 2026 01:53
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fix Plan::satisfy() to correctly append the witnessScript as the final
witness stack element for P2WSH descriptor types (Wsh, WshSortedMulti,
ShWsh, ShWshSortedMulti).

Previously, these types were incorrectly grouped with Wpkh and Tr,
which don't require a trailing witness script. This caused transactions
built using Plan::satisfy() to fail validation with "Witness program
hash mismatch" when broadcast.

The fix separates the descriptor type handling:
- Wpkh/Tr: return stack as-is (no witness script needed)
- Wsh/WshSortedMulti: append witness script, empty script_sig
- ShWpkh: return stack with unsigned_script_sig (no witness script)
- ShWsh/ShWshSortedMulti: append witness script and unsigned_script_sig

Closes rust-bitcoin#896

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add test_plan_satisfy_wsh() and test_plan_satisfy_sh_wsh() which verify
that Plan::satisfy() correctly constructs witness and script_sig for
P2WSH descriptors by calling plan.satisfy() directly and broadcasting
the resulting transaction to Bitcoin Core.

This is a regression test for rust-bitcoin#896.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@evanlinjin evanlinjin force-pushed the fix/plan-satisfy-does-not-append-witness-script-for-p2wsh branch from 026943a to 871e953 Compare February 9, 2026 01:54
@evanlinjin
Copy link
Contributor Author

One nit: can we format with the nightly toolchain like we do for the rest of the repo?

Done!

@evanlinjin evanlinjin requested a review from tcharding February 9, 2026 02:17
Copy link
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 871e953

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.

Plan::satisfy() does not append witnessScript for P2WSH descriptors

3 participants