plan: fix underreported witness and scriptSig sizes for wsh and sh() wrapping - #1046
Open
portlandhodl wants to merge 4 commits into
Open
portlandhodl wants to merge 4 commits into
portlandhodl wants to merge 4 commits into
Conversation
Plan::satisfy() appends the witness script as the final witness element for wsh and sh(wsh) descriptors (rust-bitcoin#897), but witness_size() only summed the template, underestimating the witness by the whole script (varint length prefix plus script bytes) and skewing fee estimation. Updates the affected unit tests to the corrected witness sizes. Assisted-by: Kimi kimi-latest
The scriptSig of a nested-segwit input is a push of the witness program, and scriptsig_size() missed that outer push opcode byte: sh(wpkh) is 24 bytes on the wire, not 23, and sh(wsh) is 36 bytes, not 35. Fee estimates were 4 WU per input too low. Assisted-by: Kimi kimi-latest
Adds a regression test holding witness_size() and scriptsig_size() to the exact wire size of a real satisfaction (a sigless wsh/sh(wsh) path, so signature size variance does not apply) and satisfaction_weight() to Descriptor::max_weight_to_satisfy(), plus the exact scriptSig check for sh(wpkh). Assisted-by: Kimi kimi-latest
portlandhodl
marked this pull request as draft
September 4, 2026 18:41
Member
|
What needs to be done to undraft this? |
Collaborator
Author
|
Minimum: I need to squash commits 4 + 5. Bigger picture I am investigating : while holding these sizes byte-for-byte against a real satisfaction, I found the problem doesn't stop at segwit. The legacy
Then this: the old push_slice path encoded OP_1 (thresh/or branch selectors) as OP_PUSHBYTES_1 0x01, non-minimal, so even sub-252-byte sh() scriptSigs could differ byte-for-byte from get_satisfaction(). |
portlandhodl
marked this pull request as ready for review
September 15, 2026 13:13
A plan budgets every ECDSA signature at 73 bytes including its length prefix (the convention documented on types::extra_props::SatData), so a satisfier handing out 72-byte signatures (71-byte DER plus sighash flag, filling the budget exactly) lets wpkh, sh(wpkh), wsh and sh(wsh) plans be held to the exact wire size of a real satisfaction, not just the sigless paths. Assisted-by: Kimi kimi-latest
portlandhodl
force-pushed
the
2026-09/plan-size-fixes
branch
from
September 15, 2026 13:15
e3bd963 to
ab32aa1
Compare
Abeeujah
reviewed
Sep 16, 2026
Comment on lines
259
to
276
| if self.descriptor.desc_type().segwit_version().is_some() { | ||
| witness_size(self.template.as_ref()) | ||
| let template_size = witness_size(self.template.as_ref()); | ||
| match self.descriptor.desc_type() { | ||
| DescriptorType::Wsh | DescriptorType::ShWsh => { | ||
| let script_len = self | ||
| .descriptor | ||
| .explicit_script() | ||
| .expect("wsh descriptors have explicit script") | ||
| .len(); | ||
| template_size + varint_len(script_len) + script_len | ||
| - varint_len(self.template.len()) | ||
| + varint_len(self.template.len() + 1) | ||
| } | ||
| _ => template_size, | ||
| } | ||
| } else { | ||
| 0 | ||
| } |
Contributor
There was a problem hiding this comment.
Nit, this can be done if you have to address any other changes, for scannability, I would implement the function like this:
pub fn witness_size(&self) -> usize {
let desc_type = self.descriptor.desc_type();
if desc_type.segwit_version().is_none() {
return 0;
}
let template_size = witness_size(self.template.as_ref());
match desc_type {
DescriptorType::Wsh | DescriptorType::ShWsh => {
let script_len = self
.descriptor
.explicit_script()
.expect("wsh descriptors have explicit script")
.len();
let template_len = self.template.len();
let varint_diff = varint_len(template_len + 1) - varint_len(template_len);
template_size + varint_len(script_len) + script_len + varint_diff
}
_ => template_size,
}
}- This reduces the amount of calls to
desc_typefrom two to one. - Reduces Logic nesting by early returning
0if it's not a witness program. - Separates the
varintarithmetic from the witness size arithmetic.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reimplements #1045, where these bugs were originally found and reported by @guggero.
Fixes two size-accounting bugs in
Planfee-estimation helpers that caused input sizes (and therefore fee estimates) to be underreported:Count the witness script in
witness_sizefor wsh and sh(wsh).Plan::satisfy()appends the witness script as the final witness element for wsh and sh(wsh) descriptors (Append witness script for P2WSH inPlan::satisfy()#897), butwitness_size()only summed the template, underestimating the witness by the whole script (varint length prefix plus script bytes).Fix scriptSig size for sh(wpkh) and sh(wsh). The scriptSig of a nested-segwit input is a push of the witness program, and
scriptsig_size()missed the outer push opcode byte: sh(wpkh) is 24 bytes on the wire, not 23, and sh(wsh) is 36 bytes, not 35. Fee estimates were 4 WU per input too low.Regression tests: hold
witness_size()andscriptsig_size()to the exact wire size of a real satisfaction andsatisfaction_weight()toDescriptor::max_weight_to_satisfy(), for both a sigless wsh/sh(wsh) path (no signature size variance) and — using 72-byte signatures that exactly fill the 73-byte-per-signature budget documented ontypes::extra_props::SatData— signature-bearing wpkh, sh(wpkh), wsh and sh(wsh) paths.Assisted-by: Kimi kimi-latest