Leave dust-sized change to the fee instead of creating it
`if change > 0` created a change output for any leftover at all. Below the P2WPKH dust threshold (294 sat: the output's 31 vbytes plus the 67 needed to spend it, at the 3000 sat/kvB dust relay fee) relaying nodes reject the whole transaction, so the bet or withdrawal failed at broadcast with an error the user could do nothing about — and which arrived as a 500 (BUGS.md B-06). Sub-dust change now goes to the fee in both builders, and a sub-dust recipient/winner/commission amount is refused up front with its own error code. The fee estimate already assumed two outputs, so dropping one never underpays. Cross-checked against PalladiumWallet, the source of truth for PLM parameters: it delegates to NBitcoin's TransactionBuilder (same 294 sat threshold) and has an explicit test — Un_resto_sotto_la_soglia_dust_viene_assorbito_nella_fee — asserting the same behaviour, so both the value and the semantics match the reference implementation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
from embit import script
|
||||
from embit.bip32 import HDKey
|
||||
from embit.transaction import Transaction
|
||||
|
||||
from app.wallet.plm_network import PLM_MAINNET
|
||||
from app.wallet.psbt_builder import (
|
||||
@@ -111,3 +112,75 @@ def test_build_signed_transaction_raises_when_amount_smaller_than_fee():
|
||||
change_address=my_address,
|
||||
fee_rate_sat_vb=1,
|
||||
)
|
||||
|
||||
|
||||
def test_dust_change_is_left_to_the_fee():
|
||||
"""B-06: `if change > 0` created change outputs below the dust limit, which makes
|
||||
the whole transaction unrelayable — the bet or withdrawal then failed at broadcast
|
||||
with an opaque error the user could do nothing about."""
|
||||
from app.wallet.psbt_builder import DUST_LIMIT_SATS
|
||||
|
||||
signer = _key(1)
|
||||
from_script = script.p2wpkh(signer.to_public())
|
||||
to_address = script.p2wpkh(_key(2).to_public()).address(network=PLM_MAINNET)
|
||||
change_address = script.p2wpkh(signer.to_public()).address(network=PLM_MAINNET)
|
||||
|
||||
amount = 10_000_000
|
||||
dust_change = DUST_LIMIT_SATS - 1
|
||||
built = build_signed_transaction(
|
||||
signing_key=signer,
|
||||
from_script=from_script,
|
||||
utxos=[Utxo("33" * 32, 0, amount + dust_change)],
|
||||
to_address=to_address,
|
||||
amount_sats=amount,
|
||||
change_address=change_address,
|
||||
fee_rate_sat_vb=1,
|
||||
)
|
||||
|
||||
tx = Transaction.parse(bytes.fromhex(built.raw_hex))
|
||||
assert len(tx.vout) == 1 # no dust output
|
||||
assert built.change_sats == 0
|
||||
# Nothing vanishes: the dust ends up in the fee, and inputs still equal outputs+fee.
|
||||
assert built.fee_sats >= dust_change
|
||||
assert built.recipient_sats + built.change_sats + built.fee_sats == amount + dust_change
|
||||
|
||||
|
||||
def test_change_at_the_dust_limit_is_still_paid_back():
|
||||
from app.wallet.psbt_builder import DUST_LIMIT_SATS
|
||||
|
||||
signer = _key(1)
|
||||
from_script = script.p2wpkh(signer.to_public())
|
||||
to_address = script.p2wpkh(_key(2).to_public()).address(network=PLM_MAINNET)
|
||||
change_address = script.p2wpkh(signer.to_public()).address(network=PLM_MAINNET)
|
||||
|
||||
amount = 10_000_000
|
||||
built = build_signed_transaction(
|
||||
signing_key=signer,
|
||||
from_script=from_script,
|
||||
utxos=[Utxo("44" * 32, 0, amount + DUST_LIMIT_SATS)],
|
||||
to_address=to_address,
|
||||
amount_sats=amount,
|
||||
change_address=change_address,
|
||||
fee_rate_sat_vb=1,
|
||||
)
|
||||
|
||||
assert built.change_sats == DUST_LIMIT_SATS
|
||||
assert len(Transaction.parse(bytes.fromhex(built.raw_hex)).vout) == 2
|
||||
|
||||
|
||||
def test_dust_sized_recipient_amount_is_refused():
|
||||
signer = _key(1)
|
||||
from_script = script.p2wpkh(signer.to_public())
|
||||
to_address = script.p2wpkh(_key(2).to_public()).address(network=PLM_MAINNET)
|
||||
change_address = script.p2wpkh(signer.to_public()).address(network=PLM_MAINNET)
|
||||
|
||||
with pytest.raises(InsufficientFundsError):
|
||||
build_signed_transaction(
|
||||
signing_key=signer,
|
||||
from_script=from_script,
|
||||
utxos=[Utxo("55" * 32, 0, 1_000_000)],
|
||||
to_address=to_address,
|
||||
amount_sats=400, # after the ~160 sat fee this lands under the dust limit
|
||||
change_address=change_address,
|
||||
fee_rate_sat_vb=1,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user