Browse Source

fix #6674: raise exceptions if dscancel or cpfp create dust or negative output.

master
ThomasV 5 years ago
parent
commit
7619949b2f
  1. 5
      electrum/gui/qt/history_list.py
  2. 9
      electrum/gui/qt/main_window.py
  3. 69
      electrum/wallet.py

5
electrum/gui/qt/history_list.py

@ -694,9 +694,8 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
if tx_details.can_bump: if tx_details.can_bump:
menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx)) menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx))
else: else:
child_tx = self.wallet.cpfp(tx, 0) if tx_details.can_cpfp:
if child_tx: menu.addAction(_("Child pays for parent"), lambda: self.parent.cpfp_dialog(tx))
menu.addAction(_("Child pays for parent"), lambda: self.parent.cpfp(tx, child_tx))
if tx_details.can_dscancel: if tx_details.can_dscancel:
menu.addAction(_("Cancel (double-spend)"), lambda: self.parent.dscancel_dialog(tx)) menu.addAction(_("Cancel (double-spend)"), lambda: self.parent.dscancel_dialog(tx))
invoices = self.wallet.get_relevant_invoices_for_tx(tx) invoices = self.wallet.get_relevant_invoices_for_tx(tx)

9
electrum/gui/qt/main_window.py

@ -69,7 +69,7 @@ from electrum.transaction import (Transaction, PartialTxInput,
PartialTransaction, PartialTxOutput) PartialTransaction, PartialTxOutput)
from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
sweep_preparations, InternalAddressCorruption, sweep_preparations, InternalAddressCorruption,
CannotDoubleSpendTx) CannotDoubleSpendTx, CannotCPFP)
from electrum.version import ELECTRUM_VERSION from electrum.version import ELECTRUM_VERSION
from electrum.network import (Network, TxBroadcastError, BestEffortRequestFailed, from electrum.network import (Network, TxBroadcastError, BestEffortRequestFailed,
UntrustedServerReturnedError, NetworkException) UntrustedServerReturnedError, NetworkException)
@ -3127,7 +3127,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
vbox.addLayout(Buttons(CloseButton(d))) vbox.addLayout(Buttons(CloseButton(d)))
d.exec_() d.exec_()
def cpfp(self, parent_tx: Transaction, new_tx: PartialTransaction) -> None: def cpfp_dialog(self, parent_tx: Transaction) -> None:
new_tx = self.wallet.cpfp(parent_tx, 0)
total_size = parent_tx.estimated_size() + new_tx.estimated_size() total_size = parent_tx.estimated_size() + new_tx.estimated_size()
parent_txid = parent_tx.txid() parent_txid = parent_tx.txid()
assert parent_txid assert parent_txid
@ -3210,7 +3211,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if fee > max_fee: if fee > max_fee:
self.show_error(_('Max fee exceeded')) self.show_error(_('Max fee exceeded'))
return return
try:
new_tx = self.wallet.cpfp(parent_tx, fee) new_tx = self.wallet.cpfp(parent_tx, fee)
except CannotCPFP as e:
self.show_error(str(e))
return
new_tx.set_rbf(True) new_tx.set_rbf(True)
self.show_transaction(new_tx) self.show_transaction(new_tx)

69
electrum/wallet.py

@ -218,11 +218,17 @@ def get_locktime_for_new_transaction(network: 'Network') -> int:
class CannotBumpFee(Exception): pass class CannotBumpFee(Exception):
def __str__(self):
return _('Cannot bump fee') + ':\n\n' + Exception.__str__(self)
class CannotDoubleSpendTx(Exception): pass class CannotDoubleSpendTx(Exception):
def __str__(self):
return _('Cannot cancel transaction') + ':\n\n' + Exception.__str__(self)
class CannotCPFP(Exception):
def __str__(self):
return _('Cannot create child transaction') + ':\n\n' + Exception.__str__(self)
class InternalAddressCorruption(Exception): class InternalAddressCorruption(Exception):
def __str__(self): def __str__(self):
@ -236,6 +242,7 @@ class TxWalletDetails(NamedTuple):
label: str label: str
can_broadcast: bool can_broadcast: bool
can_bump: bool can_bump: bool
can_cpfp: bool
can_dscancel: bool # whether user can double-spend to self can_dscancel: bool # whether user can double-spend to self
can_save_as_local: bool can_save_as_local: bool
amount: Optional[int] amount: Optional[int]
@ -567,6 +574,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
exp_n = None exp_n = None
can_broadcast = False can_broadcast = False
can_bump = False can_bump = False
can_cpfp = False
tx_hash = tx.txid() # note: txid can be None! e.g. when called from GUI tx dialog tx_hash = tx.txid() # note: txid can be None! e.g. when called from GUI tx dialog
is_lightning_funding_tx = False is_lightning_funding_tx = False
if self.has_lightning() and tx_hash is not None: if self.has_lightning() and tx_hash is not None:
@ -602,6 +610,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
can_bump = is_any_input_ismine and not tx.is_final() can_bump = is_any_input_ismine and not tx.is_final()
can_dscancel = (is_any_input_ismine and not tx.is_final() can_dscancel = (is_any_input_ismine and not tx.is_final()
and not all([self.is_mine(txout.address) for txout in tx.outputs()])) and not all([self.is_mine(txout.address) for txout in tx.outputs()]))
try:
self.cpfp(tx, 0)
can_cpfp = True
except:
can_cpfp = False
else: else:
status = _('Local') status = _('Local')
can_broadcast = self.network is not None can_broadcast = self.network is not None
@ -632,6 +645,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
label=label, label=label,
can_broadcast=can_broadcast, can_broadcast=can_broadcast,
can_bump=can_bump, can_bump=can_bump,
can_cpfp=can_cpfp,
can_dscancel=can_dscancel, can_dscancel=can_dscancel,
can_save_as_local=can_save_as_local, can_save_as_local=can_save_as_local,
amount=amount, amount=amount,
@ -1377,24 +1391,20 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
if not isinstance(tx, PartialTransaction): if not isinstance(tx, PartialTransaction):
tx = PartialTransaction.from_tx(tx) tx = PartialTransaction.from_tx(tx)
assert isinstance(tx, PartialTransaction) assert isinstance(tx, PartialTransaction)
if tx.is_final(): if tx.is_final():
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final')) raise CannotBumpFee(_('Transaction is final'))
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
old_tx_size = tx.estimated_size() old_tx_size = tx.estimated_size()
try: try:
# note: this might download input utxos over network # note: this might download input utxos over network
tx.add_info_from_wallet(self, ignore_network_issues=False) tx.add_info_from_wallet(self, ignore_network_issues=False)
except NetworkException as e: except NetworkException as e:
raise CannotBumpFee(_('Cannot bump fee') + ': ' + repr(e)) raise CannotBumpFee(repr(e))
old_fee = tx.get_fee() old_fee = tx.get_fee()
assert old_fee is not None assert old_fee is not None
old_fee_rate = old_fee / old_tx_size # sat/vbyte old_fee_rate = old_fee / old_tx_size # sat/vbyte
if new_fee_rate <= old_fee_rate: if new_fee_rate <= old_fee_rate:
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _("The new fee rate needs to be higher than the old fee rate.")) raise CannotBumpFee(_("The new fee rate needs to be higher than the old fee rate."))
try: try:
# method 1: keep all inputs, keep all not is_mine outputs, # method 1: keep all inputs, keep all not is_mine outputs,
# allow adding new inputs # allow adding new inputs
@ -1413,14 +1423,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
tx_new = self._bump_fee_through_decreasing_outputs( tx_new = self._bump_fee_through_decreasing_outputs(
tx=tx, new_fee_rate=new_fee_rate) tx=tx, new_fee_rate=new_fee_rate)
method_used = 2 method_used = 2
target_min_fee = new_fee_rate * tx_new.estimated_size() target_min_fee = new_fee_rate * tx_new.estimated_size()
actual_fee = tx_new.get_fee() actual_fee = tx_new.get_fee()
if actual_fee + 1 < target_min_fee: if actual_fee + 1 < target_min_fee:
raise Exception(f"bump_fee fee target was not met (method: {method_used}). " raise CannotBumpFee(
f"bump_fee fee target was not met (method: {method_used}). "
f"got {actual_fee}, expected >={target_min_fee}. " f"got {actual_fee}, expected >={target_min_fee}. "
f"target rate was {new_fee_rate}") f"target rate was {new_fee_rate}")
tx_new.locktime = get_locktime_for_new_transaction(self.network) tx_new.locktime = get_locktime_for_new_transaction(self.network)
tx_new.add_info_from_wallet(self) tx_new.add_info_from_wallet(self)
return tx_new return tx_new
@ -1450,14 +1459,14 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# all outputs are is_mine and none of them are change. # all outputs are is_mine and none of them are change.
# we bail out as it's unclear what the user would want! # we bail out as it's unclear what the user would want!
# the coinchooser bump fee method is probably not a good idea in this case # the coinchooser bump fee method is probably not a good idea in this case
raise CannotBumpFee(_('Cannot bump fee') + ': all outputs are non-change is_mine') raise CannotBumpFee(_('All outputs are non-change is_mine'))
old_not_is_mine = list(filter(lambda o: not self.is_mine(o.address), old_outputs)) old_not_is_mine = list(filter(lambda o: not self.is_mine(o.address), old_outputs))
if old_not_is_mine: if old_not_is_mine:
fixed_outputs = old_not_is_mine fixed_outputs = old_not_is_mine
else: else:
fixed_outputs = old_outputs fixed_outputs = old_outputs
if not fixed_outputs: if not fixed_outputs:
raise CannotBumpFee(_('Cannot bump fee') + ': could not figure out which outputs to keep') raise CannotBumpFee(_('Could not figure out which outputs to keep'))
if coins is None: if coins is None:
coins = self.get_spendable_coins(None) coins = self.get_spendable_coins(None)
@ -1469,7 +1478,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size) return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
coin_chooser = coinchooser.get_coin_chooser(self.config) coin_chooser = coinchooser.get_coin_chooser(self.config)
try: try:
return coin_chooser.make_tx(coins=coins, return coin_chooser.make_tx(
coins=coins,
inputs=old_inputs, inputs=old_inputs,
outputs=fixed_outputs, outputs=fixed_outputs,
change_addrs=change_addrs, change_addrs=change_addrs,
@ -1500,7 +1510,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
x_fee_address, x_fee_amount = x_fee x_fee_address, x_fee_amount = x_fee
s = filter(lambda o: o.address != x_fee_address, s) s = filter(lambda o: o.address != x_fee_address, s)
if not s: if not s:
raise CannotBumpFee(_('Cannot bump fee') + ': no outputs at all??') raise CannotBumpFee('No outputs at all??')
# prioritize low value outputs, to get rid of dust # prioritize low value outputs, to get rid of dust
s = sorted(s, key=lambda o: o.value) s = sorted(s, key=lambda o: o.value)
@ -1520,7 +1530,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# note: delta might be negative now, in which case # note: delta might be negative now, in which case
# the value of the next output will be increased # the value of the next output will be increased
if delta > 0: if delta > 0:
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs')) raise CannotBumpFee(_('Could not find suitable outputs'))
return PartialTransaction.from_io(inputs, outputs) return PartialTransaction.from_io(inputs, outputs)
@ -1531,16 +1541,19 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
if self.is_mine(address): if self.is_mine(address):
break break
else: else:
return raise CannotCPFP(_("Could not find suitable output"))
coins = self.get_addr_utxo(address) coins = self.get_addr_utxo(address)
item = coins.get(TxOutpoint.from_str(txid+':%d'%i)) item = coins.get(TxOutpoint.from_str(txid+':%d'%i))
if not item: if not item:
return raise CannotCPFP(_("Could not find coins for output"))
inputs = [item] inputs = [item]
out_address = (self.get_single_change_address_for_new_transaction(allow_reuse=False) out_address = (self.get_single_change_address_for_new_transaction(allow_reuse=False)
or self.get_unused_address() or self.get_unused_address()
or address) or address)
outputs = [PartialTxOutput.from_address_and_value(out_address, value - fee)] output_value = value - fee
if output_value < self.dust_threshold():
raise CannotCPFP(_("The output value remaining after fee is too low."))
outputs = [PartialTxOutput.from_address_and_value(out_address, output_value)]
locktime = get_locktime_for_new_transaction(self.network) locktime = get_locktime_for_new_transaction(self.network)
tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime) tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
tx_new.add_info_from_wallet(self) tx_new.add_info_from_wallet(self)
@ -1558,22 +1571,19 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
assert isinstance(tx, PartialTransaction) assert isinstance(tx, PartialTransaction)
if tx.is_final(): if tx.is_final():
raise CannotDoubleSpendTx(_('Cannot cancel transaction') + ': ' + _('transaction is final')) raise CannotDoubleSpendTx(_('Transaction is final'))
new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision
old_tx_size = tx.estimated_size() old_tx_size = tx.estimated_size()
try: try:
# note: this might download input utxos over network # note: this might download input utxos over network
tx.add_info_from_wallet(self, ignore_network_issues=False) tx.add_info_from_wallet(self, ignore_network_issues=False)
except NetworkException as e: except NetworkException as e:
raise CannotDoubleSpendTx(_('Cannot cancel transaction') + ': ' + repr(e)) raise CannotDoubleSpendTx(repr(e))
old_fee = tx.get_fee() old_fee = tx.get_fee()
assert old_fee is not None assert old_fee is not None
old_fee_rate = old_fee / old_tx_size # sat/vbyte old_fee_rate = old_fee / old_tx_size # sat/vbyte
if new_fee_rate <= old_fee_rate: if new_fee_rate <= old_fee_rate:
raise CannotDoubleSpendTx(_('Cannot cancel transaction') + ': ' + _("The new fee rate needs to be higher than the old fee rate.")) raise CannotDoubleSpendTx(_("The new fee rate needs to be higher than the old fee rate."))
# grab all ismine inputs # grab all ismine inputs
inputs = [txin for txin in tx.inputs() inputs = [txin for txin in tx.inputs()
if self.is_mine(self.get_txin_address(txin))] if self.is_mine(self.get_txin_address(txin))]
@ -1582,9 +1592,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
old_change_addrs = [o.address for o in tx.outputs() if self.is_mine(o.address)] old_change_addrs = [o.address for o in tx.outputs() if self.is_mine(o.address)]
out_address = (self.get_single_change_address_for_new_transaction(old_change_addrs) out_address = (self.get_single_change_address_for_new_transaction(old_change_addrs)
or self.get_receiving_address()) or self.get_receiving_address())
locktime = get_locktime_for_new_transaction(self.network) locktime = get_locktime_for_new_transaction(self.network)
outputs = [PartialTxOutput.from_address_and_value(out_address, value)] outputs = [PartialTxOutput.from_address_and_value(out_address, value)]
tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime) tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
new_tx_size = tx_new.estimated_size() new_tx_size = tx_new.estimated_size()
@ -1593,6 +1601,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
old_fee + self.relayfee() * new_tx_size / Decimal(1000), # BIP-125 rules 3 and 4 old_fee + self.relayfee() * new_tx_size / Decimal(1000), # BIP-125 rules 3 and 4
) )
new_fee = int(math.ceil(new_fee)) new_fee = int(math.ceil(new_fee))
output_value = value - new_fee
if output_value < self.dust_threshold():
raise CannotDoubleSpendTx(_("The output value remaining after fee is too low."))
outputs = [PartialTxOutput.from_address_and_value(out_address, value - new_fee)] outputs = [PartialTxOutput.from_address_and_value(out_address, value - new_fee)]
tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime) tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
tx_new.add_info_from_wallet(self) tx_new.add_info_from_wallet(self)

Loading…
Cancel
Save