[BIP Draft] Worst-Case Taproot Witness Weight Estimation

13 views
Skip to first unread message

Jan Thomasewsky

unread,
Apr 11, 2026, 5:55:50 PM (6 hours ago) Apr 11
to Bitcoin Development Mailing List
BIP: ?
Layer: Applications
Title: Worst-Case Taproot Witness Weight Estimation
Author: Jan Thomasewsky <jan_...@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
Reply all
Reply to author
Forward
0 new messages