From: Jan Thomasewsky <programaciong730@gmail.com>
To: Bitcoin Development Mailing List <bitcoindev@googlegroups.com>
Subject: [bitcoindev] [BIP Draft] Worst-Case Taproot Witness Weight Estimation
Date: Sat, 11 Apr 2026 14:54:27 -0700 (PDT) [thread overview]
Message-ID: <46489794-cce9-436f-854c-33828140e218n@googlegroups.com> (raw)
[-- Attachment #1.1: Type: text/plain, Size: 8184 bytes --]
BIP: ?
Layer: Applications
Title: Worst-Case Taproot Witness Weight Estimation
Author: Jan Thomasewsky <jan_btcln@proton.me>
Status: Draft
Type: Standards Track
Created: 2026-04-11
License: CC-BY-4.0
Requires: 341, 380, 386
Abstract
This document specifies how wallet implementations should estimate the
witness
weight of inputs spending BIP 341 Taproot outputs when constructing
transactions. Existing implementations assume key path spending for all
tr() inputs, which underestimates the weight when a script path spend is
required and causes the transaction to broadcast at a lower effective fee
rate than the user intended.
Motivation
When constructing a transaction a wallet must estimate the witness weight of
each input to calculate the correct fee. For a tr(KEY, TREE) output the
witness differs substantially depending on which spending path is used.
A key path spend requires a single Schnorr signature. The witness
contribution, excluding the witness element count varint, is 66 weight units
(1-byte compact-size length prefix + 64-byte Schnorr signature + 1-byte
sighash flag for the worst-case non-default sighash type).
A script path spend requires the satisfaction of the chosen leaf, the leaf
script itself, and a control block of:
1 + 32 + 32 * depth
bytes. For a simple pk(X) leaf at depth 0 the witness contribution is 135
weight units, more than twice the key path size, and for deeper trees or
more
complex scripts the gap widens further.
Current wallet implementations, including Bitcoin Core, return the key path
weight for any tr() input regardless of whether the wallet holds the
internal
key. When the internal key is unavailable and the wallet must spend via a
script path, the transaction is undersized in the fee estimate.
In a congested mempool this can push the effective fee rate below the
minimum
relay threshold or below the fee rate required for timely inclusion in a
block, leaving the transaction unconfirmed indefinitely. This also weakens
the reliability of RBF fee bumping, since the initial transaction anchors
future replacement fees too low.
For example, a 1-in-2-out transaction spending a tr(KEY_A, pk(KEY_B)) output
via the script path at a target of 10 sat/vbyte would be estimated at 131
vbytes and broadcast with a fee of 1310 sats, but its actual size is 148
vbytes, giving an effective fee rate of 8.85 sat/vbyte.
More generally, this specification formalises the principle that fee
estimation should be descriptor-aware rather than script-type-aware. A
wallet
that has a full tr() descriptor can compute a deterministic worst-case
weight
without heuristics, aligning fee estimation with the exact set of possible
satisfactions encoded in the descriptor.
Specification
Wallet software that constructs transactions spending tr() outputs MUST
compute the maximum witness weight for each input as specified below.
This estimator is conservative by construction and does not assume knowledge
of the eventual spending path at transaction construction time. All sizes in
this section are expressed in witness weight units.
The returned value excludes the witness element count varint, which is
accounted for separately by the transaction weight calculation layer.
Weight calculation
Let TREE be the set of tapleaves in the descriptor, each with an associated
depth in the Merkle tree.
Define:
- sat_size(leaf): maximum byte size of the witness elements satisfying the
leaf script, including the compact-size varint prefix of each element but
excluding the witness element count varint.
- script_size(leaf): byte size of the serialized leaf script.
- depth(leaf): depth of the leaf in the Merkle tree (root = 0).
- varint(n): byte length of the compact-size encoding of n.
Pseudocode:
best = 66 # key path: 1 + 64 + 1
for each leaf in TREE:
if sat_size(leaf) is unknown or script_size(leaf) is unknown:
continue
cb_size = 1 + 32 + 32 * depth(leaf)
leaf_weight = sat_size(leaf) \
+ varint(script_size(leaf)) + script_size(leaf) \
+ varint(cb_size) + cb_size
if leaf_weight > best:
best = leaf_weight
return best
If all leaves are skipped due to unknown sizes, the function returns 66.
Implementations encountering this case with a non-empty TREE SHOULD log a
warning, as it typically indicates an incomplete descriptor.
If the wallet holds the internal key and can confirm it is usable for
signing, it SHOULD return 66 directly without evaluating the leaves.
Descriptor availability
This algorithm requires the full tr() descriptor including the script tree.
Implementations that reconstruct descriptors from on-chain data or from a
signing provider that does not retain tree information may lose the tree
structure, causing the estimator to fall back to a key-path-only result.
Implementations MUST use the stored descriptor rather than a reconstructed
one when estimating input weight.
Rationale
Worst-case weight
Using the maximum across all available spending paths ensures the
transaction
is never broadcast below the target fee rate. Overestimation results in a
slightly higher fee, which is preferable to underestimation that can leave
transactions stuck.
This mirrors the worst-case satisfaction model already used for other
descriptor types such as wsh() with multiple satisfaction paths.
Witness stack count
The witness element count varint is excluded for consistency with existing
descriptor interfaces, where it is handled by the transaction assembly
layer.
Fallback when tree is unknown
Returning the key path weight when no leaf sizes can be computed preserves
backwards-compatible behaviour for tr(KEY) descriptors. Distinguishing
between key-path-only and incomplete descriptors is left to the caller.
Security Considerations
Fee underestimation
Wallets that do not implement this specification may broadcast transactions
at a lower effective fee rate than intended. For complex trees, the mismatch
can exceed 100%, making confirmation unlikely.
Fee overestimation
This specification always returns a worst-case weight. If a lighter path is
used, the user overpays the difference. Implementations MAY adjust the fee
after path selection.
Malformed descriptors
Descriptors with very large trees or deep nesting may cause excessive CPU
usage or inflated fee estimates. Implementations SHOULD enforce reasonable
limits.
Backwards Compatibility
This proposal does not affect consensus or network behaviour. Wallets that
adopt it produce more accurate fee estimates. Others continue to function
but
remain subject to underestimation.
Test Vectors
All values exclude the witness element count varint.
Single leaf at depth 0
Descriptor: tr(KEY_A, pk(KEY_B))
- sat_size = 66
- script_size = 34
- depth = 0
- cb_size = 33
- leaf_weight = 135
Key path weight = 66
Result: 135
Two leaves at depth 1
Descriptor: tr(KEY_A, {pk(KEY_B), multi_a(2,KEY_C,KEY_D)})
Leaf 1 (pk):
- cb_size = 65
- leaf_weight = 167
Leaf 2 (multi_a):
- script_size = 68
- sat_size = 132
- cb_size = 65
- leaf_weight = 267
Result: 267
Key path only
Descriptor: tr(KEY_A)
Result: 66
Reference Implementation
A reference implementation can be achieved with the following changes to
Bitcoin Core:
- In src/script/descriptor.cpp, update TRDescriptor::MaxSatisfactionWeight
and TRDescriptor::MaxSatisfactionElems to iterate over all tapleaves and
return the maximum weight.
- In src/wallet/spend.cpp, update GetDescriptor() to prefer stored
descriptors over InferDescriptor(), which loses the script tree and causes
fallback to rawtr().
References
BIP 340 - Schnorr Signatures for secp256k1
BIP 341 - Taproot
BIP 380 - Output Script Descriptors
BIP 386 - tr() Descriptors
--
You received this message because you are subscribed to the Google Groups "Bitcoin Development Mailing List" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bitcoindev+unsubscribe@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/bitcoindev/46489794-cce9-436f-854c-33828140e218n%40googlegroups.com.
[-- Attachment #1.2: Type: text/html, Size: 9735 bytes --]
reply other threads:[~2026-04-11 21:55 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=46489794-cce9-436f-854c-33828140e218n@googlegroups.com \
--to=programaciong730@gmail.com \
--cc=bitcoindev@googlegroups.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox