wallet: anchor reserve: handle wallet.use_change being off
What changed, and why it matters
This commit fixes a crash in Electrum's transaction builder when a user has Lightning anchor channels enabled but has disabled change addresses. Previously, the code assumed a change address would always be available and tried to access the first item of an empty list, causing an error. The fix falls back to reusing an input address for the required reserve output when no change address exists.
No immediate security response needed; this is a robustness fix. Users with Lightning anchor channels and use_change disabled should update to avoid transaction-building failures. Review whether address reuse for reserve UTXOs has privacy implications for such wallets.
Security signals we found
Denial-of-service vector: unhandled IndexError crashes transaction creation
Address reuse as intentional fallback for Lightning UTXO reserve requirement
Fixes a user-reported bug (issue #10231) but no explicit security framing by vendor
Evidence from the diff
In wallet.py, get_change_addresses_for_new_transaction() can return an empty list when wallet.use_change is False. The anchor-channel UTXO reserve logic called [0] on this list unconditionally, raising an IndexError. The patch checks if the list is empty and, if so, reuses the first transaction input’s address for the reserve output. A regression test confirms the fallback behavior.
Changed components
electrum/wallet.pytests/test_wallet_vertical.pyInspect captured patch +24 / −1
diff --git a/electrum/wallet.py b/electrum/wallet.py
index 70648ec..060de90 100644
--- a/electrum/wallet.py
+++ b/electrum/wallet.py
@@ -1850,6 +1850,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def get_change_addresses_for_new_transaction(
self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
) -> List[str]:
+ """note: might return an empty list! (e.g. if use_change is disabled, or allow_reuse is False)"""
change_addrs = []
if preferred_change_addr:
if isinstance(preferred_change_addr, (list, tuple)):
@@ -2111,7 +2112,8 @@ class Abstract_Wallet(ABC, Logger, EventListener):
to_distribute -= reserve_sized_input.value_sats()
else:
self.logger.info(f'Adding change output to meet utxo reserve requirements')
- change_addr = self.get_change_addresses_for_new_transaction(change_addr)[0]
+ change_addrs = self.get_change_addresses_for_new_transaction(change_addr)
+ change_addr = change_addrs[0] if change_addrs else tx_inputs[0].address
change = PartialTxOutput.from_address_and_value(change_addr, self.config.LN_UTXO_RESERVE)
change.is_utxo_reserve = True # for GUI
outputs.append(change)
diff --git a/tests/test_wallet_vertical.py b/tests/test_wallet_vertical.py
index c1c73dc..2578362 100644
--- a/tests/test_wallet_vertical.py
+++ b/tests/test_wallet_vertical.py
@@ -2107,6 +2107,27 @@ class TestWalletSending(ElectrumTestCase):
tx = make_tx(to_self_address)
self.assertEqual(1, len(tx.outputs()))
+ async def test_ln_reserve__usechange_off(self):
+ """Send all the coins using 'max', with wallet.use_change being off.
+ This will create a reserve UTXO, reusing an input address.
+ """
+ wallet, outgoing_address, to_self_address = self._create_cause_carbon_wallet()
+ wallet.use_change = False
+ def make_tx():
+ outputs = [PartialTxOutput.from_address_and_value(outgoing_address, '!')]
+ wallet.lnworker = mock.Mock()
+ wallet.lnworker.has_anchor_channels.return_value = True
+ return wallet.make_unsigned_transaction(
+ outputs = outputs,
+ fee_policy = FixedFeePolicy(100),
+ )
+ tx = make_tx()
+ self.assertEqual(1, len(tx.inputs()))
+ self.assertEqual(2, len(tx.outputs()))
+ outputs = {txout.address: txout.value for txout in tx.outputs()}
+ assert outgoing_address in outputs
+ assert outputs[tx.inputs()[0].address] == self.config.LN_UTXO_RESERVE # address-reuse
+
async def test_ln_reserve_keep_existing_reserve(self):
"""
tests if make_unsigned_transaction keeps the existing reserve utxo
Why this scored 34/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.