bitcoin/tx.c: use 330 sat dust limit for P2TR/P2WPKH change outputs Fixes #8395 Fix by checking is_elements: Elements keeps 546 sat, Bitcoin uses 330 sat. Changelog-Fixed: Transactions now correctly create change outputs >= 330 sat for P2TR/P2WPKH instead of absorbing them as fees
What changed, and why it matters
This commit fixes a bug in Core Lightning where small change outputs on Bitcoin were being treated as too tiny to keep, causing them to be silently added to transaction fees instead of returned to the user. The fix lowers the threshold for P2TR/P2WPKH change outputs on Bitcoin from 546 satoshis to 330 satoshis, matching current Bitcoin network rules. Elements/Liquid still uses 546. Users could previously lose small change amounts as extra fees.
Review whether any other output types or fee-estimation paths use the same stale dust limit. Consider adding equivalent tests for P2WPKH change and for Elements behavior. No urgent security patch deployment is required, but the fix should be included in the next release.
Security signals we found
Incorrect dust-limit threshold caused legitimate change outputs to be converted to fees
User funds could be lost as transaction fees under specific output sizes
Fix is network-specific (Bitcoin vs Elements) to avoid changing Liquid behavior
Regression test added for P2TR change dust boundary
Evidence from the diff
In bitcoin/tx.c’s change_amount(), the code previously compared the change amount against chainparams->dust_limit (546 sat) for all networks. On Bitcoin mainnet, the dust limit for a P2TR or P2WPKH output is actually 330 sat, so change between 330 and 546 sat was discarded and absorbed into the fee. The patch branches on chainparams->is_elements: Elements keeps 546 sat, Bitcoin uses 330 sat. A regression test creates a P2TR change output between 330 and 546 sat and verifies it is preserved.
Changed components
bitcoin/tx.cchange_amount()P2TR/P2WPKH change output creationBitcoin network transaction fundingInspect captured patch +61 / −3
diff --git a/bitcoin/tx.c b/bitcoin/tx.c
index fe25b10..dabd525 100644
--- a/bitcoin/tx.c
+++ b/bitcoin/tx.c
@@ -985,9 +985,13 @@ struct amount_sat change_amount(struct amount_sat excess, u32 feerate_perkw,
if (!amount_sat_sub(&excess, excess, fee))
return AMOUNT_SAT(0);
- /* Must be non-dust */
- if (!amount_sat_greater_eq(excess, chainparams->dust_limit))
- return AMOUNT_SAT(0);
+ if (chainparams->is_elements) {
+ if (!amount_sat_greater_eq(excess, AMOUNT_SAT(546)))
+ return AMOUNT_SAT(0);
+ } else {
+ if (!amount_sat_greater_eq(excess, AMOUNT_SAT(330)))
+ return AMOUNT_SAT(0);
+ }
return excess;
}
diff --git a/tests/test_p2tr_change_dust.py b/tests/test_p2tr_change_dust.py
new file mode 100644
index 0000000..bffb04b
--- /dev/null
+++ b/tests/test_p2tr_change_dust.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+"""Test P2TR change outputs with dust limit 330 sat (issue #8395)."""
+import unittest
+
+from fixtures import * # noqa: F401,F403
+from fixtures import TEST_NETWORK
+from utils import wait_for
+
+
+@unittest.skipIf(TEST_NETWORK == 'liquid-regtest', "P2TR not yet supported on Elements")
+def test_p2tr_change_dust_limit(node_factory, bitcoind):
+
+ l1 = node_factory.get_node(feerates=(253, 253, 253, 253))
+
+ addr = l1.rpc.newaddr('p2tr')['p2tr']
+ bitcoind.rpc.sendtoaddress(addr, 1.0)
+ bitcoind.generate_block(1)
+ wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)
+
+ outputs = l1.rpc.listfunds()['outputs']
+ assert len(outputs) == 1
+ utxo = outputs[0]
+
+ utxo_amount = int(utxo['amount_msat'] / 1000)
+
+ target_amount = utxo_amount - 450
+
+ result = l1.rpc.fundpsbt(
+ satoshi=f"{target_amount}sat",
+ feerate="253perkw",
+ startweight=0,
+ excess_as_change=True
+ )
+
+ assert 'change_outnum' in result, "Expected change output to be created"
+
+ psbt = bitcoind.rpc.decodepsbt(result['psbt'])
+
+ change_outnum = result['change_outnum']
+ if 'tx' in psbt:
+ change_output = psbt['tx']['vout'][change_outnum]
+ change_amount_btc = float(change_output['value'])
+ else:
+ change_output = psbt['outputs'][change_outnum]
+ change_amount_btc = float(change_output['amount'])
+
+ change_amount_sat = int(change_amount_btc * 100_000_000)
+
+ print(f"Change amount: {change_amount_sat} sat")
+
+ assert change_amount_sat >= 330, f"Change {change_amount_sat} sat should be >= 330 sat"
+ assert change_amount_sat <= 546, f"Change {change_amount_sat} sat should be <= 546 sat (for this test)"
+
+ print(f"SUCCESS: P2TR change output of {change_amount_sat} sat created (between 330 and 546 sat)")
Why this scored 36/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.