Skip to content

fix: do not create change or split outputs below Bitcoin Core dust limit - #1

Open
limpbrains wants to merge 1 commit into
masterfrom
fix/core-dust-threshold
Open

limpbrains wants to merge 1 commit into
masterfrom
fix/core-dust-threshold

Conversation

@limpbrains

@limpbrains limpbrains commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Problem

Change created when remainder > 148 * feeRate. At 1 sat/vB that is 149 sats. Bitcoin Core dust limit is 546 (p2pkh) / 540 (p2sh) / 294 (p2wpkh) / 330 (p2wsh, p2tr). Tx with such change rejected by nodes as dust. Same for split (send max) outputs. Known upstream, never fixed: bitcoinjs#86, bitcoinjs#16.

Fix

  • New utils.isDust(value, output, feeRate): dust if value <= 148 * feeRate (not worth spending, strict as before) or value < Core dust limit for output type (inclusive, same as Core IsDust). Core limit = same math as GetDustThreshold(), dustrelayfee 3 sat/vB. No script = p2pkh. Used by finalize (change) and split.
  • New optional 4th arg options for all algos + finalize:
    • changeScript: { length } - real size of change output + its dust limit. Omitted = p2pkh, old behaviour.
    • txExtraBytes: number - tx bytes lib does not know about, e.g. 1 for segwit marker & flag.
  • Invalid options (wrong types, unknown keys like { length: 34 }) = no solution {}. Silently ignoring them burned whole change as fee. utils.transactionBytes returns NaN for them. changeScript.length over 10000 (Core MAX_SCRIPT_SIZE) rejected too.
  • blackjack overpay tolerance stays 148 * feeRate. Tying it to dust limit made it burn up to 546 sats when solution with change was cheaper (test added).
  • index.d.ts, README updated.

Backwards compatible: 3-arg calls behave the same outside sub-dust window. Below-dust change goes to fee instead.

Check

  • npm test: 258 pass, standard clean, coverage 100%. No old fixture changed.
  • Differential fuzz old vs new, 300k random cases, half steered to dust boundary: 258354 identical. 41646 differ, all the same way: change < 546 dropped, same inputs, fee up by exactly that change. 0 other diffs. 0 cases where old found solution and new did not. Dust change 0.
  • With options (248k solved): change never below limit of its type, fee never below feeRate * size.
  • split with exact script lengths: 668 cases old solved and new did not, all 668 were real dust (0 relayable).

Notes

  • Witness output detected by script length (22 / 34), lib only gets length. Unknown length = non-witness = higher limit = safe side. Caller must pass exact scriptPubKey length, padded length (e.g. +3) gets the non-witness limit.
  • Known leftover: dropping sub-dust change can cost more than adding one more input would. Old code made unrelayable tx there. Follow-up for accumulative.

…mit, add changeScript and txExtraBytes options
@limpbrains
limpbrains force-pushed the fix/core-dust-threshold branch from 98899e7 to 8f2fcc7 Compare September 18, 2026 17:30

@GladosBlueWallet GladosBlueWallet left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test subject, your dust patch does what the commit message promises—how refreshingly rare. utils.isDust and relayDustThreshold now enforce Bitcoin Core dust limits (plus the existing spend-cost check) in finalize and split; sub-dust scraps get incinerated into fees instead of spawning relay-rejected outputs. changeScript and txExtraBytes propagate through every algorithm; checkOptions returns {} on bad input. blackjack keeps its separate 148 * feeRate overpay tolerance—two experiments, one facility. Three-argument callers stay compatible outside the sub-dust window. README and index.d.ts document the new behavior. Tests hit real selection/finalize paths with concrete values, not mocks. Diff trace: no regressions. Proceed. For now.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reevaluating 'Dust' Considerations When Adding Change in Transactions

2 participants