Follow BIP379 for the thresh "e" rule - #1032
Conversation
BIP379 computes the "e" (expressive) property of a threshold as "all are s"; this crate additionally requires every child to be "e", as Bitcoin Core did until bitcoin/bitcoin#36028. The three disagree in no other malleability or correctness rule: an independent implementation of the BIP379 tables agrees with this crate on all 36,370 expressions of the corpora in ms_tests.rs, except for the 20 thresholds this commit retypes. The extra condition cannot change any outcome a caller can act on. It is already part of `non_malleable` below, and non-malleability is conjunctive, so a threshold whose children are all "s" but not all "e" is malleable either way, as is every expression containing one. The difference is only visible in the type of an expression that is malleable to begin with. It does matter for exact type test vectors, which is where it surfaced: BIP379's test vectors are being contributed from these corpora (bitcoin/bips#2005), so the specification and the implementations have to agree on the type of a malleable expression as well. Note that the property description in the BIP ("a unique unconditional dissatisfaction") does not hold for these thresholds: `or_i(pk(A),pk(B))` is a legal thresh child which is "s" but not "e", and a third party can freely pick either of its two dissatisfactions. The BIP is being amended to note that the descriptions only bind for expressions which meet the malleability requirements.
Covers both sides of the rule the previous commit changed, on the smallest expression that discriminates: a threshold whose children are all "s" but not all "e" is "e" and malleable, and one with a child that is not "s" is neither.
|
Interesting. Okay, I think this is reasonable. The claim is that, assuming that all children are
Therefore we can just always set The alternative (fix the BIP to match the implementation) might work, but it'd be upholding a stronger promise than we intended to make and would require far more extensive analysis for little benefit. See bitcoin/bips#2267 (comment) |
|
Now I'm kinda musing about changing the pub struct Malleability {
pub dissat: Dissat,
pub signed: bool,
pub non_malleable: bool,
}we'd have pub enum Malleability {
Malleable,
NonMalleable {
dissat: Dissat,
signed: bool,
},
}But I guess doing this would break our test vectors for little benefit. If we ever provide a "user-facing API" for the type system maybe we can do something like this. |
| // A threshold all of whose children are signed, but not all of | ||
| // which have a unique dissatisfaction, is Unique here even though a | ||
| // third party can vary the dissatisfaction of such a child. This is | ||
| // BIP379's rule ("e=all are s") and it cannot mislead a caller: |
There was a problem hiding this comment.
In 439be85:
lmao "cannot mislead a caller"
Let's tighten up this comment to say "Intuitively, Dissat::Unique should require all children to be Dissat::Unique. But this does not match BIP 379. Notice that if any child is not Dissat::Unique, the entire thresh will be malleable so the dissat property has no meaning anyway. See PR 1032 and BIPs PR 2267 for more discussion."
|
Closing in favor of #1037, which seems to be the preferred option. |
The three malleability properties of a fragment only describe it if the
fragment is non-malleable to begin with, so carrying them for a malleable
one invites reading a promise into them that the type system does not
make. Encode that in the type:
pub enum Malleability {
Malleable,
NonMalleable { dissat: Dissat, signed: bool },
}
Every combinator already required each of its sub expressions to be
non-malleable, so a malleable sub expression short circuits the whole
fragment and the requirement that the BIP379 table lists for each
fragment becomes an early return rather than a conjunct buried in a
`non_malleable` field. No non-malleable expression changes type, and
nothing but the erased properties of a malleable expression changes at
all.
Two consequences worth naming:
- `requires_sig` returns false for a malleable expression, where it used
to return a value the type system did not stand behind. False is the
answer that keeps a caller from relying on a signature being required.
It is spelled as a single `matches!` on the enum rather than a defaulted
`Option`, so the conjunction it reports is visible at the definition.
The attribute tests assert it for every vector, expecting `non_mal &&
need_sig` rather than skipping the malleable ones, and the next commit
renames and deprecates the method.
- The threshold "e" rule now matches BIP379's "e=all are s" exactly,
without the extra condition of rust-bitcoin#1032: the requirement checked before it
already establishes that every sub expression is "e", so the two rules
coincide for every threshold that has the property at all.
`Malleability` is public, so this is a breaking change for anyone reading
its fields.
Assisted-by: Claude Opus 5
Prompted by bitcoin/bips#2240 (comment).
Related: bitcoin/bitcoin#36028 and bitcoin/bips#2267.
Description
BIP379 computes the "e" (expressive) property of a threshold as
e=all are s.This crate additionally requires every child to be "e". Bitcoin Core did the
same until bitcoin/bitcoin#36028, which aligns it with the BIP; this does the
same for rust-miniscript.
Why it doesn't change anything a caller can act on
The extra condition is already part of
non_malleable, and non-malleability isconjunctive, so a threshold whose children are all "s" but not all "e" is
malleable either way — as is every expression containing one. Wherever an
mrule consumes a child's
eit also requires that child'sm, so nomvaluechanges anywhere, and
Descriptor::from_straccepts and rejects exactly what itdid before. Only the type of an expression that was already malleable changes.
Why it matters anyway
BIP379's test vectors are being contributed from the corpora in
ms_tests.rs(bitcoin/bips#2240), and exact type vectors mean the specification and the
implementations have to agree on the type of a malleable expression too.
To check that this is the only rule where they disagree, I implemented the
BIP379 correctness and malleability tables as an independent reference and ran
it over the five corpora with recorded types — 36,370 expressions, including the
malleable and timelock-conflicting ones:
threshe rulems_tests.rsall are s(BIP text)all are e and all are s(this crate, before)So this crate matched the BIP in every other correctness and malleability rule.
(Core additionally differed in
or_b's "e" and in two vacuousd:rules, whichis the rest of #36028.)
A note on the property description
BIP379 describes "e" as requiring "a unique unconditional dissatisfaction to
exist", and that does not hold under its own rule:
or_i(pk(A),pk(B))is a legalthresh child which is "s" but not "e", and spending
or_d(thresh(2,or_i(pk(A),pk(B)),a:or_i(pk(C),pk(D))),pk(E))through its secondbranch has four consensus-valid witnesses that differ only in the two
branch-selector bytes. The BIP is being amended to note that the property
descriptions only bind for expressions meeting the malleability requirements.