Skip to content

DrainWeights::dust_threshold is below Bitcoin Core's dust limit for P2TR change #81

Open

Description

@noahjoeris

Describe the bug

DrainWeights::dust_threshold computes the threshold from the drain's actual output_weight + spend_weight. For DrainWeights::TR_KEYSPEND that is 172 + 230 = 402 WU, which at the default 3 sat/vB dust relay feerate gives 302 sats. Bitcoin Core's dust rule for segwit outputs instead assumes a fixed 67 vB spend size, which is why a P2TR output is dust below 330 sats (matching the crate's own TR_DUST_RELAY_MIN_VALUE).

Since LowestFee::drain_value relies on dust_threshold, it can return a P2TR change output valued between 302 and 329 sats, which default Bitcoin Core relay policy treats as dust.

This issue was found by AI.

To Reproduce

use bdk_coin_select::{
    metrics::LowestFee, BnbMetric, Candidate, CoinSelector, DrainWeights, FeeRate, Target,
    TargetFee, TargetOutputs, TR_DUST_RELAY_MIN_VALUE,
};

#[test]
fn does_not_create_p2tr_change_below_core_dust_threshold() {
    let candidates = [Candidate {
        value: 1_000,
        weight: 1,
        input_count: 1,
        is_segwit: true,
    }];
    let mut cs = CoinSelector::new(&candidates);
    assert!(cs.select(0));

    let target = Target {
        fee: TargetFee::ZERO,
        outputs: TargetOutputs {
            value_sum: 1_000 - (TR_DUST_RELAY_MIN_VALUE - 10),
            weight_sum: 1,
            n_outputs: 1,
        },
        max_weight: None,
    };
    let mut metric = LowestFee {
        long_term_feerate: FeeRate::from_sat_per_vb(1.0),
        dust_relay_feerate: FeeRate::from_sat_per_vb(3.0),
        drain_weights: DrainWeights::TR_KEYSPEND,
    };

    // Fails: returns a drain with value 320.
    assert!(metric.drain(&cs, target).is_none());
}

Expected behavior

The dust threshold used by LowestFee should not produce change outputs that Bitcoin Core's default dust relay policy rejects (i.e. below 330 sats for P2TR at 3 sat/vB).

Activity

  1. Ansh-699 commented on Oct 21, 2026

    @Ansh-699

    I can pick this up. The test in the report doesn't compile anymore after #59 and #63, but I updated it and it still fails on dcb1ce9. dust_threshold comes out at 302 sats for TR_KEYSPEND at 3 sat/vB, and LowestFee hands back 320 sats of P2TR change. Core's dust limit for P2TR is 330.

    Looks like this started with 28878e6. Before that, the caller passed the limit in through ChangePolicy. The catch is that Core doesn't look at the real spend weight at all. It just assumes 67 vB for witness programs and 148 vB for everything else, so output_weight + spend_weight can't line up with it unless we know the script type.

    I see two ways to go. The small fix is to never let the spend side drop below Core's 67 vB. That keeps P2WPKH, P2WSH and P2TR change at or above Core's limit, but P2SH-wrapped change would still come in under it. The other way is to have the caller pass the dust limit in (e.g. from rust-bitcoin's Script::minimal_non_dust_custom), like ChangePolicy::min_value used to. That's exact for every script type, but it means changing LowestFee again before 0.5.0.

    Which one would you rather have?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions