txbatcher: don't spend anchors if ctx fee is sufficient
What changed, and why it matters
This commit tweaks how Electrum handles small backup transaction inputs called 'anchors.' Anchors are meant to help a transaction get mined faster by adding extra fee. The change says: if the original transaction already pays enough fee on its own, don't bother spending the anchor. This is a logic improvement, not a clear security fix, but it removes a scenario where the wallet might create an unnecessary follow-up transaction that could waste money or behave unexpectedly.
Treat as a routine correctness/optimization patch. Reviewers should verify that get_tx_fee and estimate_fee behave correctly for unconfirmed or low-fee transactions, and that skipping the anchor does not leave any intended fee-bumping path unfulfilled. No urgent security action is indicated from the diff alone.
Security signals we found
Avoids unnecessary anchor input consumption when existing transaction fee is already sufficient
Prevents potential creation of redundant low-fee sweep transactions
Uses only_once logging to reduce noise
Evidence from the diff
In electrum/txbatcher.py, the TxBatch loop that processes batch_inputs now fetches the previous transaction (prev_tx) and, for anchor inputs, compares the current transaction fee against the estimated target fee. If the current fee already exceeds the target, it skips spending the anchor. The patch also changes a boolean-only existence check to capture the transaction object. The change is defensive and avoids creating redundant anchor-spending transactions, but it does not obviously close a remote-exploitable vulnerability.
Changed components
electrum/txbatcher.pyTxBatch batch input processinganchor sweep logicInspect captured patch +16 / −1
diff --git a/electrum/txbatcher.py b/electrum/txbatcher.py
index ee1727e..48279b4 100644
--- a/electrum/txbatcher.py
+++ b/electrum/txbatcher.py
@@ -329,7 +329,7 @@ class TxBatch(Logger):
for prevout, sweep_info in list(self.batch_inputs.items()):
assert prevout == sweep_info.txin.prevout
prev_txid, index = prevout.to_str().split(':')
- if not self.wallet.adb.db.get_transaction(prev_txid):
+ if not (prev_tx := self.wallet.adb.db.get_transaction(prev_txid)):
continue
if sweep_info.is_anchor():
prev_tx_mined_status = self.wallet.adb.get_tx_height(prev_txid)
@@ -337,6 +337,21 @@ class TxBatch(Logger):
self.logger.info(f"anchor not needed {prevout}")
self.batch_inputs.pop(prevout) # note: if the input is already in a batch tx, this will trigger assert error
continue
+ prev_tx_current_fee = self.wallet.adb.get_tx_fee(prev_txid)
+ try:
+ prev_tx_target_fee = self.fee_policy.estimate_fee(
+ prev_tx.estimated_size(),
+ network=self.wallet.network,
+ )
+ except NoDynamicFeeEstimates:
+ prev_tx_target_fee = None
+ fees_available = prev_tx_current_fee and prev_tx_target_fee
+ if fees_available and prev_tx_current_fee > prev_tx_target_fee:
+ self.logger.info(
+ f"not using anchor now, fee sufficient: "
+ f"{prev_tx_current_fee=} > {prev_tx_target_fee=}", only_once=True,
+ )
+ continue
if spender_txid := self.wallet.adb.db.get_spent_outpoint(prev_txid, int(index)):
tx_mined_status = self.wallet.adb.get_tx_height(spender_txid)
if tx_mined_status.height() not in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:
Why this scored 26/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.