Skip to content

refactor: remove SortedMultiVec and use Terminal::SortedMulti#915

Merged
apoelstra merged 1 commit intorust-bitcoin:masterfrom
trevarj:refactor-sortedmulti
Apr 12, 2026
Merged

refactor: remove SortedMultiVec and use Terminal::SortedMulti#915
apoelstra merged 1 commit intorust-bitcoin:masterfrom
trevarj:refactor-sortedmulti

Conversation

@trevarj
Copy link
Copy Markdown
Contributor

@trevarj trevarj commented Apr 4, 2026

  • Utilize the Miniscript parsing to handle sortedmulti as a Terminal.
  • Deleted sortedmulti.rs (SortedMultiVec)
  • Refactor Wsh to only wrap a Miniscript now that SortedMultiVec isn't used.
  • Refactor ShInner to remove SortedMulti variant and only use the Ms variant for
    sortedmulti scripts

Now that sortedmulti_a is supported as a Terminal, I think it makes sense to
move sortedmulti over in the same way by following what multi does and applying
the sorting functions that were introduced in eba1aff.

Like sortedmulti_a, sortedmulti is sorted upon encoding and cannot be decoded
into from Script.

@trevarj trevarj force-pushed the refactor-sortedmulti branch from 2c77295 to 9b0b1c0 Compare April 4, 2026 13:14
@apoelstra
Copy link
Copy Markdown
Member

(CI is failing because the integration tests don't compile)

Hmmmm, okay, this seems reasonable enough to me. Does Core allow sortedmulti in arbitrary positions or only as a top-level descriptor?

@trevarj
Copy link
Copy Markdown
Contributor Author

trevarj commented Apr 4, 2026

(CI is failing because the integration tests don't compile)

Hmmmm, okay, this seems reasonable enough to me. Does Core allow sortedmulti in arbitrary positions or only as a top-level descriptor?

Working on fixing the integration tests now! Will ping you when it's ready, my bad.

@trevarj trevarj force-pushed the refactor-sortedmulti branch from 9b0b1c0 to 816620a Compare April 4, 2026 14:54
Descriptor::Sh(sh) => match sh.as_inner() {
miniscript::descriptor::ShInner::Wsh(wsh) => match wsh.as_inner() {
miniscript::descriptor::WshInner::SortedMulti(ref smv) => {
let ms = Miniscript::from_ast(smv.sorted_node()).unwrap();
Copy link
Copy Markdown
Contributor Author

@trevarj trevarj Apr 4, 2026

Choose a reason for hiding this comment

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

The key point here was to call sorted_node() and now we don't have something so convenient. See the HACK in find_sks_ms

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Interesting. So, the issue is that we want to get all the keys in the descriptor, in the order they'd be needed, and we're using pk_iter for this. Since pk_iter can't sort the keys, we have to sort them after the fact.

This is a bit of a weird thing to desire -- does this test actually fail without this? It seems like we never use the ordering of sks_reqd. In fact, we lose the ordering because we iterate over it, produce signatures, then stick them into the BTreeMap psbt.inputs[0].partial_sigs.

Also, since #910 I think the logic in this example is actually wrong, or at least inconsistent -- it would sort sortedmulti but not sortedmulti_a. And your hack doesn't change this.

So can we just drop the sortedmulti logic here and treat sortedmulti like multi?

Comment thread src/descriptor/mod.rs Outdated
ShInner::Wsh(ref wsh) => {
let ms = wsh.as_inner();
if let Terminal::SortedMulti(ref thresh) = ms.node {
PkIter::from_sortedmulti(thresh.data())
Copy link
Copy Markdown
Contributor Author

@trevarj trevarj Apr 4, 2026

Choose a reason for hiding this comment

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

Cannot call thresh.into_sorted_bip67() due to lacking ToPublicKey on Pk here. It would clean up the integration test changes (HACK). I'm sure it would be useful for users as well.

Copy link
Copy Markdown
Member

@apoelstra apoelstra Apr 9, 2026

Choose a reason for hiding this comment

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

Right -- I get the desire that pk_iter return the keys in the correct order. But as you note, without a ToPublicKey bound, we don't actually know the correct order (and the user might not either, e.g. if there are wildcards in the keys).

We also can't make this do the right thing for ToPublicKey keys because Rust lacks specialization. (There are tons of places in this crate where we suffer from this, and you'll find that if you keep trying to clean stuff up, you will run into missing language features basically every single time.)

I think we need to just live with this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are tons of places in this crate where we suffer from this, and you'll find that if you keep trying to clean stuff up, you will run into missing language features basically every single time.

@apoelstra don't worry, in 2035 when specialization stabilizes we can get to it!

Comment thread src/descriptor/mod.rs
}

#[test]
fn too_many_pubkeys_for_p2sh() {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FYI: migrated from deleted sortedmulti.rs file.

Comment thread src/descriptor/segwitv0.rs Outdated
let mut witness = match self.inner {
WshInner::SortedMulti(ref smv) => smv.satisfy(satisfier)?,
WshInner::Ms(ref ms) => ms.satisfy_malleable(satisfier)?,
let mut witness = if let Terminal::SortedMulti(..) = self.ms.node {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All the if let Terminal::SortedMulti(..)'s are kind of bothering me.

Does it make sense to add a function on Miniscript called is_sortedmulti() or something?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Probably yes, but I'm curious what the heck is going on here. Why are we calling the non-malleable satisfier for sortedmultis? (What I suspect is: the old SortedMultiVec type had a satisfy method but no satisfy_malleable method, presumably because for multisigs, malleability is irrelevant. We needed to branch because SortedMultiVec is a distinct type, and then the branch wound up looking sus with one side saying satisfy and the other side saying satisfy_malleable, just because of this API gap.)

SO I think that in this PR, we just always want to call self.ms.satisfy_malleable, and not branch at all.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I based it off of this, which is from the deleted SortedMultiVec.

-    /// Attempt to produce a satisfying witness for the
-    /// witness script represented by the parse tree
-    pub fn satisfy<S>(&self, satisfier: S) -> Result<Vec<Vec<u8>>, Error>
-    where
-        Pk: ToPublicKey,
-        S: Satisfier<Pk>,
-    {
-        let ms = Miniscript::from_ast(self.sorted_node()).expect("Multi node typecheck");
-        ms.satisfy(satisfier)

We needed to branch because SortedMultiVec is a distinct type, and then the branch wound up looking sus with one side saying satisfy and the other side saying satisfy_malleable, just because of this API gap.

I don't think I could have guessed that lol...trusting you and changing to only call satisfy_malleable

@trevarj
Copy link
Copy Markdown
Contributor Author

trevarj commented Apr 5, 2026

@apoelstra did a self-review with some questions. Let me know what you think, thanks!

@apoelstra
Copy link
Copy Markdown
Member

816620a needs rebase

@trevarj trevarj force-pushed the refactor-sortedmulti branch from 816620a to f9d0bdd Compare April 9, 2026 17:02
@apoelstra
Copy link
Copy Markdown
Member

f9d0bdd needs rebase

@trevarj trevarj force-pushed the refactor-sortedmulti branch 3 times, most recently from b4d73ec to 366974f Compare April 9, 2026 17:32
Comment thread src/descriptor/mod.rs Outdated
ShInner::Wsh(ref wsh) => {
let ms = wsh.as_inner();
if let Terminal::SortedMulti(ref thresh) = ms.node {
PkIter::from_sortedmulti(thresh.data())
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are tons of places in this crate where we suffer from this, and you'll find that if you keep trying to clean stuff up, you will run into missing language features basically every single time.

@apoelstra don't worry, in 2035 when specialization stabilizes we can get to it!

Comment thread src/descriptor/segwitv0.rs Outdated
let mut witness = match self.inner {
WshInner::SortedMulti(ref smv) => smv.satisfy(satisfier)?,
WshInner::Ms(ref ms) => ms.satisfy_malleable(satisfier)?,
let mut witness = if let Terminal::SortedMulti(..) = self.ms.node {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I based it off of this, which is from the deleted SortedMultiVec.

-    /// Attempt to produce a satisfying witness for the
-    /// witness script represented by the parse tree
-    pub fn satisfy<S>(&self, satisfier: S) -> Result<Vec<Vec<u8>>, Error>
-    where
-        Pk: ToPublicKey,
-        S: Satisfier<Pk>,
-    {
-        let ms = Miniscript::from_ast(self.sorted_node()).expect("Multi node typecheck");
-        ms.satisfy(satisfier)

We needed to branch because SortedMultiVec is a distinct type, and then the branch wound up looking sus with one side saying satisfy and the other side saying satisfy_malleable, just because of this API gap.

I don't think I could have guessed that lol...trusting you and changing to only call satisfy_malleable

Comment thread src/miniscript/iter.rs
Comment on lines +35 to +38
| Terminal::Multi(_)
| Terminal::SortedMulti(_)
| Terminal::MultiA(_)
| Terminal::SortedMultiA(_) => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Saw SortedMultiA was also missing from this.

Comment thread src/miniscript/iter.rs
match (&self.node, n) {
(Terminal::PkK(key), 0) | (Terminal::PkH(key), 0) => Some(key.clone()),
(Terminal::Multi(thresh), _) => thresh.data().get(n).cloned(),
(Terminal::SortedMulti(thresh), _) => thresh.data().get(n).cloned(),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I missed this on the first iteration since type-driven development failed me with the _ => fallback.

@apoelstra It resolves the HACK that was mentioned in this comment.

Comment thread src/miniscript/mod.rs
Comment on lines +663 to +665
Terminal::Multi(ref thresh) | Terminal::SortedMulti(ref thresh)
if !thresh.iter().all(&mut pred) =>
{
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After rebasing one of the tests from 1062144 caught that I missed this! Thanks @febyeji

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great!

@trevarj trevarj force-pushed the refactor-sortedmulti branch from 366974f to 941205b Compare April 9, 2026 18:01
Copy link
Copy Markdown
Contributor

@febyeji febyeji left a comment

Choose a reason for hiding this comment

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

I left a couple of comments. One on missing SortedMulti arms in context.rs and one stale comment nit.

Comment thread src/miniscript/context.rs Outdated
Comment thread src/miniscript/context.rs Outdated
Comment thread src/miniscript/context.rs Outdated
Comment thread src/miniscript/context.rs Outdated
Comment thread src/descriptor/sh.rs Outdated
@trevarj trevarj force-pushed the refactor-sortedmulti branch from 941205b to a65deea Compare April 10, 2026 12:26
@trevarj
Copy link
Copy Markdown
Contributor Author

trevarj commented Apr 10, 2026

I left a couple of comments. One on missing SortedMulti arms in context.rs and one stale comment nit.

Thanks again, @febyeji ! Can't believe I missed so many match arms 😞

Comment thread src/descriptor/mod.rs Outdated
ShInner::Wsh(ref wsh) => {
let ms = wsh.as_inner();
if let Terminal::SortedMulti(ref thresh) = ms.node {
PkIter::from_sortedmulti(thresh.data())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In a65deea:

Here, and below, you can just delete the from_sortedmulti method and get rid of this branch. It is now covered by the from_miniscript_* methods.

Comment thread src/descriptor/mod.rs
},
ShInner::Wsh(ref wsh) => {
if let Terminal::SortedMulti(..) = wsh.as_inner().node {
DescriptorType::ShWshSortedMulti
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In a65deea:

As a followup let's maybe delete this from DescriptorType since it's no longer a distinct kind of descriptor. But let's not do it here since it'd be an API-breaking change and I'm not certain that we want to do it.

@apoelstra
Copy link
Copy Markdown
Member

Done reviewing a65deea. All good except the iterpk thing. Frustrating how many matches there are in this crate that have _ fields. In general it seems super hard to find them all.

- Utilize the Miniscript parsing to handle sortedmulti as a Terminal.
- Deleted sortedmulti.rs (SortedMultiVec)
- Refactor Wsh to only wrap a Miniscript now that SortedMultiVec isn't used.
- Refactor ShInner to remove SortedMulti variant and only use the Ms variant for
  sortedmulti scripts
@trevarj trevarj force-pushed the refactor-sortedmulti branch from a65deea to 01d25c1 Compare April 10, 2026 17:54
@trevarj trevarj requested review from apoelstra and febyeji April 10, 2026 17:55
@apoelstra
Copy link
Copy Markdown
Member

Nice :) great to see such a red diff.

Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

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

ACK 01d25c1; successfully ran local tests

@apoelstra apoelstra merged commit ddc42c7 into rust-bitcoin:master Apr 12, 2026
26 checks passed
Comment thread src/miniscript/satisfy.rs
}
}
Terminal::Multi(ref thresh) => {
Terminal::Multi(ref thresh) | Terminal::SortedMulti(ref thresh) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oops, I missed this -- in this case we need to change the logic. In SortedMultiA we have special logic to sort keys, but in SortedMulti we don't.

We should also add a test case for this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I will fix this in #895 -- though if you have an earlier fix I'd appreciate since that PR is likely to be in limbo for at least several more weeks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, big oversight on my part...I will fix that and a test. I guess we test it in the bitcoind tests but not using our own satisfier.

apoelstra added a commit that referenced this pull request Apr 13, 2026
3f20a69 fix: add sorting to satisfy_helper for sortedmulti (Trevor Arjeski)

Pull request description:

  This logic was done in sortedmulti_a but accidentally overlooked when
  refactoring sortedmulti.
  
  Wrote a test that proves two differently ordered sortedmulti descriptors produce
  the same witness.
  
  Found by Andrew in #915 (comment)


ACKs for top commit:
  apoelstra:
    ACK 3f20a69; successfully ran local tests


Tree-SHA512: f81642afeb8415aec564260d352176cf479f316fae1e07f2b9d455449d4749224c0bb05b51f7836718d43cdfe59387062596d4282c9b208bb1b2ae651e19da79
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.

3 participants