Browse Source

wallet: add sighash check to class Abstract_Wallet

qml: use backend sighash check and add user confirmation path
qt: use backend sighash check and add user confirmation path
qml: include get_warning_for_risk_of_burning_coins_as_fees test in txdetails
qt: add warning icon to sighash warning
add sighash and fee checks to wallet.sign_transaction, making all warnings fatal unless ignore_warnings is set to True
tests: test sign_transaction on both code paths with ignore_warnings True and False,
raise specific exceptions TransactionPotentiallyDangerousException and TransactionDangerousException
master
Sander van Grieken 2 years ago committed by accumulator
parent
commit
7b96a83350
  1. 4
      electrum/commands.py
  2. 25
      electrum/gui/qml/components/TxDetails.qml
  3. 27
      electrum/gui/qml/qetxdetails.py
  4. 3
      electrum/gui/qml/qewallet.py
  5. 3
      electrum/gui/qt/main_window.py
  6. 41
      electrum/gui/qt/transaction_dialog.py
  7. 24
      electrum/tests/test_wallet_vertical.py
  8. 1
      electrum/transaction.py
  9. 61
      electrum/wallet.py

4
electrum/commands.py

@ -469,10 +469,10 @@ class Commands:
return tx.serialize() return tx.serialize()
@command('wp') @command('wp')
async def signtransaction(self, tx, password=None, wallet: Abstract_Wallet = None): async def signtransaction(self, tx, password=None, wallet: Abstract_Wallet = None, iknowwhatimdoing: bool=False):
"""Sign a transaction. The wallet keys will be used to sign the transaction.""" """Sign a transaction. The wallet keys will be used to sign the transaction."""
tx = tx_from_any(tx) tx = tx_from_any(tx)
wallet.sign_transaction(tx, password) wallet.sign_transaction(tx, password, ignore_warnings=iknowwhatimdoing)
return tx.serialize() return tx.serialize()
@command('') @command('')

25
electrum/gui/qml/components/TxDetails.qml

@ -49,6 +49,16 @@ Pane {
text: qsTr('On-chain Transaction') text: qsTr('On-chain Transaction')
} }
InfoTextArea {
id: warn
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.bottomMargin: constants.paddingLarge
visible: txdetails.warning
text: txdetails.warning
iconStyle: InfoTextArea.IconStyle.Warn
}
InfoTextArea { InfoTextArea {
id: bumpfeeinfo id: bumpfeeinfo
Layout.columnSpan: 2 Layout.columnSpan: 2
@ -336,7 +346,20 @@ Pane {
icon.source: '../../icons/key.png' icon.source: '../../icons/key.png'
text: qsTr('Sign') text: qsTr('Sign')
visible: txdetails.canSign visible: txdetails.canSign
onClicked: txdetails.sign() onClicked: {
if (txdetails.shouldConfirm) {
var dialog = app.messageDialog.createObject(app, {
text: qsTr('Confirm signing non-standard transaction?'),
yesno: true
})
dialog.accepted.connect(function() {
txdetails.sign()
})
dialog.open()
} else {
txdetails.sign()
}
}
} }
FlatButton { FlatButton {

27
electrum/gui/qml/qetxdetails.py

@ -5,7 +5,7 @@ from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.util import format_time, TxMinedInfo from electrum.util import format_time, TxMinedInfo
from electrum.transaction import tx_from_any, Transaction from electrum.transaction import tx_from_any, Transaction, PartialTxInput, Sighash, PartialTransaction
from electrum.network import Network from electrum.network import Network
from electrum.address_synchronizer import TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE from electrum.address_synchronizer import TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE
@ -32,6 +32,7 @@ class QETxDetails(QObject, QtEventListener):
self._txid = '' self._txid = ''
self._rawtx = '' self._rawtx = ''
self._label = '' self._label = ''
self._warning = ''
self._tx = None # type: Optional[Transaction] self._tx = None # type: Optional[Transaction]
@ -56,6 +57,7 @@ class QETxDetails(QObject, QtEventListener):
self._is_mined = False self._is_mined = False
self._is_final = False self._is_final = False
self._lock_delay = 0 self._lock_delay = 0
self._should_confirm = False
self._mempool_depth = '' self._mempool_depth = ''
@ -140,6 +142,10 @@ class QETxDetails(QObject, QtEventListener):
def status(self): def status(self):
return self._status return self._status
@pyqtProperty(str, notify=detailsChanged)
def warning(self):
return self._warning
@pyqtProperty(QEAmount, notify=detailsChanged) @pyqtProperty(QEAmount, notify=detailsChanged)
def amount(self): def amount(self):
return self._amount return self._amount
@ -240,6 +246,10 @@ class QETxDetails(QObject, QtEventListener):
def lockDelay(self): def lockDelay(self):
return self._lock_delay return self._lock_delay
@pyqtProperty(bool, notify=detailsChanged)
def shouldConfirm(self):
return self._should_confirm
def update(self, from_txid: bool = False): def update(self, from_txid: bool = False):
assert self._wallet assert self._wallet
@ -284,6 +294,9 @@ class QETxDetails(QObject, QtEventListener):
fee_per_kb = txinfo.fee / size * 1000 fee_per_kb = txinfo.fee / size * 1000
self._feerate_str = self._wallet.wallet.config.format_fee_rate(fee_per_kb) self._feerate_str = self._wallet.wallet.config.format_fee_rate(fee_per_kb)
self._should_confirm = False
should_reject = False
self._lock_delay = 0 self._lock_delay = 0
self._is_mined = False if not txinfo.tx_mined_status else txinfo.tx_mined_status.height > 0 self._is_mined = False if not txinfo.tx_mined_status else txinfo.tx_mined_status.height > 0
if self._is_mined: if self._is_mined:
@ -293,6 +306,10 @@ class QETxDetails(QObject, QtEventListener):
self._mempool_depth = self._wallet.wallet.config.depth_tooltip(txinfo.mempool_depth_bytes) self._mempool_depth = self._wallet.wallet.config.depth_tooltip(txinfo.mempool_depth_bytes)
elif txinfo.tx_mined_status.height == TX_HEIGHT_FUTURE: elif txinfo.tx_mined_status.height == TX_HEIGHT_FUTURE:
self._lock_delay = txinfo.tx_mined_status.wanted_height - self._wallet.wallet.adb.get_local_height() self._lock_delay = txinfo.tx_mined_status.wanted_height - self._wallet.wallet.adb.get_local_height()
if isinstance(self._tx, PartialTransaction):
self._should_confirm, should_reject, message = self._wallet.wallet.check_sighash(self._tx)
if message:
self._warning = '\n'.join([_('Danger! This transaction is non-standard!'), message])
if self._wallet.wallet.lnworker: if self._wallet.wallet.lnworker:
# Calling lnworker.get_onchain_history and wallet.get_full_history here # Calling lnworker.get_onchain_history and wallet.get_full_history here
@ -320,7 +337,13 @@ class QETxDetails(QObject, QtEventListener):
self._can_cpfp = txinfo.can_cpfp and not txinfo.can_remove self._can_cpfp = txinfo.can_cpfp and not txinfo.can_remove
self._can_save_as_local = txinfo.can_save_as_local and not txinfo.can_remove self._can_save_as_local = txinfo.can_save_as_local and not txinfo.can_remove
self._can_remove = txinfo.can_remove self._can_remove = txinfo.can_remove
self._can_sign = not self._is_complete and self._wallet.wallet.can_sign(self._tx) self._can_sign = not self._is_complete and self._wallet.wallet.can_sign(self._tx) and not should_reject
if isinstance(self._tx, PartialTransaction):
risk_of_burning_coins = (self._can_sign and txinfo.fee is not None
and self._wallet.wallet.get_warning_for_risk_of_burning_coins_as_fees(self._tx))
if risk_of_burning_coins:
self._warning = '\n'.join([self._warning, risk_of_burning_coins])
self.detailsChanged.emit() self.detailsChanged.emit()

3
electrum/gui/qml/qewallet.py

@ -512,7 +512,8 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
def do_sign(self, tx, broadcast): def do_sign(self, tx, broadcast):
try: try:
tx = self.wallet.sign_transaction(tx, self.password) # ignore_warnings=True, because UI checks and asks user confirmation itself
tx = self.wallet.sign_transaction(tx, self.password, ignore_warnings=True)
except BaseException as e: except BaseException as e:
self._logger.error(f'{e!r}') self._logger.error(f'{e!r}')
self.signFailed.emit(str(e)) self.signFailed.emit(str(e))

3
electrum/gui/qt/main_window.py

@ -1270,7 +1270,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
# can sign directly # can sign directly
task = partial(tx.sign, external_keypairs) task = partial(tx.sign, external_keypairs)
else: else:
task = partial(self.wallet.sign_transaction, tx, password) # ignore_warnings=True, because UI checks and asks user confirmation itself
task = partial(self.wallet.sign_transaction, tx, password, ignore_warnings=True)
msg = _('Signing transaction...') msg = _('Signing transaction...')
WaitingDialog(self, msg, task, on_success, on_failure) WaitingDialog(self, msg, task, on_success, on_failure)

41
electrum/gui/qt/transaction_dialog.py

@ -50,8 +50,7 @@ from electrum.bitcoin import base_encode, NLOCKTIME_BLOCKHEIGHT_MAX, DummyAddres
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum import simple_config from electrum import simple_config
from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, TxOutpoint from electrum.transaction import SerializationError, Transaction, PartialTransaction, TxOutpoint, TxinDataFetchProgress
from electrum.transaction import TxinDataFetchProgress
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.util import ShortID, get_asyncio_loop from electrum.util import ShortID, get_asyncio_loop
from electrum.network import Network from electrum.network import Network
@ -77,15 +76,16 @@ _logger = get_logger(__name__)
dialogs = [] # Otherwise python randomly garbage collects the dialogs... dialogs = [] # Otherwise python randomly garbage collects the dialogs...
class TxSizeLabel(QLabel): class TxSizeLabel(QLabel):
def setAmount(self, byte_size): def setAmount(self, byte_size):
self.setText(('x %s bytes =' % byte_size) if byte_size else '') self.setText(('x %s bytes =' % byte_size) if byte_size else '')
class TxFiatLabel(QLabel): class TxFiatLabel(QLabel):
def setAmount(self, fiat_fee): def setAmount(self, fiat_fee):
self.setText(('%s' % fiat_fee) if fiat_fee else '') self.setText(('%s' % fiat_fee) if fiat_fee else '')
class QTextBrowserWithDefaultSize(QTextBrowser): class QTextBrowserWithDefaultSize(QTextBrowser):
def __init__(self, width: int = 0, height: int = 0): def __init__(self, width: int = 0, height: int = 0):
self._width = width self._width = width
@ -96,8 +96,8 @@ class QTextBrowserWithDefaultSize(QTextBrowser):
def sizeHint(self): def sizeHint(self):
return QSize(self._width, self._height) return QSize(self._width, self._height)
class TxInOutWidget(QWidget):
class TxInOutWidget(QWidget):
def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'):
QWidget.__init__(self) QWidget.__init__(self)
@ -113,9 +113,24 @@ class TxInOutWidget(QWidget):
self.inputs_textedit.setContextMenuPolicy(Qt.CustomContextMenu) self.inputs_textedit.setContextMenuPolicy(Qt.CustomContextMenu)
self.inputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_inputs) self.inputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_inputs)
self.sighash_label = QLabel()
self.sighash_label.setStyleSheet('font-weight: bold')
self.sighash_confirm = False
self.sighash_reject = False
self.sighash_message = ''
self.inputs_warning_icon = QLabel()
pixmap = QPixmap(icon_path("warning"))
pixmap_size = round(2 * char_width_in_lineedit())
pixmap = pixmap.scaled(pixmap_size, pixmap_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.inputs_warning_icon.setPixmap(pixmap)
self.inputs_warning_icon.setVisible(False)
self.inheader_hbox = QHBoxLayout() self.inheader_hbox = QHBoxLayout()
self.inheader_hbox.setContentsMargins(0, 0, 0, 0) self.inheader_hbox.setContentsMargins(0, 0, 0, 0)
self.inheader_hbox.addWidget(self.inputs_header) self.inheader_hbox.addWidget(self.inputs_header)
self.inheader_hbox.addStretch(2)
self.inheader_hbox.addWidget(self.sighash_label)
self.inheader_hbox.addWidget(self.inputs_warning_icon)
self.txo_color_recv = TxOutputColoring( self.txo_color_recv = TxOutputColoring(
legend=_("Wallet Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receiving address")) legend=_("Wallet Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receiving address"))
@ -246,6 +261,13 @@ class TxInOutWidget(QWidget):
short_id=str(txin.short_id), addr=addr, value=txin_value, short_id=str(txin.short_id), addr=addr, value=txin_value,
) )
if isinstance(self.tx, PartialTransaction):
self.sighash_confirm, self.sighash_reject, self.sighash_message = self.wallet.check_sighash(self.tx)
if self.sighash_message:
self.sighash_label.setText(_('Danger! This transaction is non-standard!'))
self.inputs_warning_icon.setVisible(True)
self.inputs_warning_icon.setToolTip(self.sighash_message)
self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs()))
o_text = self.outputs_textedit o_text = self.outputs_textedit
o_text.clear() o_text.clear()
@ -645,6 +667,14 @@ class TxDialog(QDialog, MessageBoxMixin):
self.update() self.update()
self.main_window.pop_top_level_window(self) self.main_window.pop_top_level_window(self)
if self.io_widget.sighash_confirm:
if not self.question('\n'.join([
_('Danger! This transaction is non-standard!'),
self.io_widget.sighash_message,
'',
_('Are you sure you want to sign this transaction?')
])):
return
self.sign_button.setDisabled(True) self.sign_button.setDisabled(True)
self.main_window.push_top_level_window(self) self.main_window.push_top_level_window(self)
self.main_window.sign_tx(self.tx, callback=sign_done, external_keypairs=self.external_keypairs) self.main_window.sign_tx(self.tx, callback=sign_done, external_keypairs=self.external_keypairs)
@ -771,7 +801,8 @@ class TxDialog(QDialog, MessageBoxMixin):
self.broadcast_button.setEnabled(tx_details.can_broadcast) self.broadcast_button.setEnabled(tx_details.can_broadcast)
can_sign = not self.tx.is_complete() and \ can_sign = not self.tx.is_complete() and \
(self.wallet.can_sign(self.tx) or bool(self.external_keypairs)) (self.wallet.can_sign(self.tx) or bool(self.external_keypairs))
self.sign_button.setEnabled(can_sign) self.sign_button.setEnabled(can_sign and not self.io_widget.sighash_reject)
self.sign_button.setToolTip(self.io_widget.sighash_message)
if tx_details.txid: if tx_details.txid:
self.tx_hash_e.setText(tx_details.txid) self.tx_hash_e.setText(tx_details.txid)
else: else:

24
electrum/tests/test_wallet_vertical.py

@ -12,12 +12,10 @@ from electrum import SimpleConfig
from electrum import util from electrum import util
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet,
restore_wallet_from_text, Abstract_Wallet, CannotBumpFee, BumpFeeStrategy) restore_wallet_from_text, Abstract_Wallet, CannotBumpFee, BumpFeeStrategy,
from electrum.util import ( TransactionPotentiallyDangerousException, TransactionDangerousException)
bfh, NotEnoughFunds, UnrelatedTransactionException, from electrum.util import bfh, NotEnoughFunds, UnrelatedTransactionException, UserFacingException
UserFacingException) from electrum.transaction import Transaction, PartialTxOutput, tx_from_any
from electrum.transaction import (TxOutput, Transaction, PartialTransaction, PartialTxOutput,
PartialTxInput, tx_from_any, TxOutpoint)
from electrum.mnemonic import seed_type from electrum.mnemonic import seed_type
from electrum.network import Network from electrum.network import Network
@ -2963,13 +2961,23 @@ class TestWalletSending(ElectrumTestCase):
partial_tx) partial_tx)
# load tx into cosignerB's offline wallet # load tx into cosignerB's offline wallet
tx = tx_from_any(partial_tx) tx = tx_from_any(partial_tx)
wallet1b_offline.sign_transaction(tx, password=None) wallet1b_offline.sign_transaction(tx, password=None, ignore_warnings=True)
self.assertEqual('02000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e100000000d9004730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf4590147304402203ba7cc21e407ce31c1eecd11c367df716a5d47f06e0bf7109f08063ede25a364022039f6bef0dd401aa2c3103b8cbab57cc4fed3905ccb0a726dc6594bf5930ae0b401475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152aefdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500', self.assertEqual('02000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e100000000d9004730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf4590147304402203ba7cc21e407ce31c1eecd11c367df716a5d47f06e0bf7109f08063ede25a364022039f6bef0dd401aa2c3103b8cbab57cc4fed3905ccb0a726dc6594bf5930ae0b401475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152aefdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500',
str(tx)) str(tx))
self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.txid()) self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.txid())
self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.wtxid()) self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.wtxid())
# again, but raise on warnings (here: signing non-segwit inputs is risky)
tx = tx_from_any(partial_tx)
try:
wallet1b_offline.sign_transaction(tx, password=None)
self.assertFalse(uses_qr_code2)
except TransactionDangerousException:
raise
except TransactionPotentiallyDangerousException:
self.assertTrue(uses_qr_code2)
@mock.patch.object(wallet.Abstract_Wallet, 'save_db') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
async def test_we_dont_sign_tx_including_dummy_address(self, mock_save_db): async def test_we_dont_sign_tx_including_dummy_address(self, mock_save_db):
wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver') wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver')
@ -3095,7 +3103,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
self.assertEqual(tx.txid(), tx_copy.txid()) self.assertEqual(tx.txid(), tx_copy.txid())
# sign tx # sign tx
tx = wallet_offline.sign_transaction(tx_copy, password=None) tx = wallet_offline.sign_transaction(tx_copy, password=None, ignore_warnings=True)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertFalse(tx.is_segwit()) self.assertFalse(tx.is_segwit())
self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.txid()) self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.txid())

1
electrum/transaction.py

@ -42,6 +42,7 @@ import copy
from . import ecc, bitcoin, constants, segwit_addr, bip32 from . import ecc, bitcoin, constants, segwit_addr, bip32
from .bip32 import BIP32Node from .bip32 import BIP32Node
from .i18n import _
from .util import profiler, to_bytes, bfh, chunks, is_hex_str, parse_max_spend from .util import profiler, to_bytes, bfh, chunks, is_hex_str, parse_max_spend
from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160,
hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr,

61
electrum/wallet.py

@ -71,7 +71,7 @@ from .storage import StorageEncryptionVersion, WalletStorage
from .wallet_db import WalletDB from .wallet_db import WalletDB
from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32 from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32
from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput, from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint) PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint, Sighash)
from .plugin import run_hook from .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE, TX_TIMESTAMP_INF) TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE, TX_TIMESTAMP_INF)
@ -233,20 +233,29 @@ class CannotBumpFee(CannotRBFTx):
def __str__(self): def __str__(self):
return _('Cannot bump fee') + ':\n\n' + Exception.__str__(self) return _('Cannot bump fee') + ':\n\n' + Exception.__str__(self)
class CannotDoubleSpendTx(CannotRBFTx): class CannotDoubleSpendTx(CannotRBFTx):
def __str__(self): def __str__(self):
return _('Cannot cancel transaction') + ':\n\n' + Exception.__str__(self) return _('Cannot cancel transaction') + ':\n\n' + Exception.__str__(self)
class CannotCPFP(Exception): class CannotCPFP(Exception):
def __str__(self): def __str__(self):
return _('Cannot create child transaction') + ':\n\n' + Exception.__str__(self) return _('Cannot create child transaction') + ':\n\n' + Exception.__str__(self)
class InternalAddressCorruption(Exception): class InternalAddressCorruption(Exception):
def __str__(self): def __str__(self):
return _("Wallet file corruption detected. " return _("Wallet file corruption detected. "
"Please restore your wallet from seed, and compare the addresses in both files") "Please restore your wallet from seed, and compare the addresses in both files")
class TransactionPotentiallyDangerousException(Exception): pass
class TransactionDangerousException(TransactionPotentiallyDangerousException): pass
class BumpFeeStrategy(enum.Enum): class BumpFeeStrategy(enum.Enum):
PRESERVE_PAYMENT = enum.auto() PRESERVE_PAYMENT = enum.auto()
DECREASE_PAYMENT = enum.auto() DECREASE_PAYMENT = enum.auto()
@ -2454,7 +2463,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
txout.is_change = self.is_change(address) txout.is_change = self.is_change(address)
self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix) self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)
def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]: def sign_transaction(self, tx: Transaction, password, *, ignore_warnings: bool = False) -> Optional[PartialTransaction]:
""" returns tx if successful else None """ """ returns tx if successful else None """
if self.is_watching_only(): if self.is_watching_only():
return return
@ -2467,6 +2476,24 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if swap: if swap:
self.lnworker.swap_manager.sign_tx(tx, swap) self.lnworker.swap_manager.sign_tx(tx, swap)
return tx return tx
# check if signing is dangerous
should_confirm, should_reject, message = self.check_sighash(tx)
if should_reject:
raise TransactionDangerousException('Not signing transaction:\n' + message)
if not ignore_warnings:
if should_confirm:
message = '\n'.join([_('Danger! This transaction is non-standard!'), message])
fee = self.get_wallet_delta(tx).fee
risk_of_burning_coins = (fee is not None
and self.get_warning_for_risk_of_burning_coins_as_fees(tx))
if risk_of_burning_coins:
should_confirm = True
message = '\n'.join([message, risk_of_burning_coins])
if should_confirm:
raise TransactionPotentiallyDangerousException('Not signing transaction:\n' + message)
# add info to a temporary tx copy; including xpubs # add info to a temporary tx copy; including xpubs
# and full derivation paths as hw keystores might want them # and full derivation paths as hw keystores might want them
tmp_tx = copy.deepcopy(tx) tmp_tx = copy.deepcopy(tx)
@ -3041,6 +3068,36 @@ class Abstract_Wallet(ABC, Logger, EventListener):
"do not accept to sign it more than once,\n" "do not accept to sign it more than once,\n"
"otherwise you could end up paying a different fee.")) "otherwise you could end up paying a different fee."))
def check_sighash(self, tx: 'PartialTransaction') -> (bool, bool, str):
"""Checks the Sighash for my inputs and return hints in the form
(confirm, reject, message), where confirm indicates the user should explicitly confirm,
reject indicates the transaction should be rejected (unless overruled in e.g. a config property),
and message containing a user-facing message describing the context"""
hintmap = dict([
(0, (False, False, None)),
(Sighash.NONE, (True, True, _('Input {} is marked SIGHASH_NONE.'))),
(Sighash.SINGLE, (True, False, _('Input {} is marked SIGHASH_SINGLE.'))),
(Sighash.ALL, (False, False, None)),
(Sighash.ANYONECANPAY, (True, False, _('Input {} is marked SIGHASH_ANYONECANPAY.')))
])
confirm = False
reject = False
messages = []
for txin_idx, txin in enumerate(tx.inputs()):
if txin.sighash and txin.sighash != Sighash.ALL: # non-standard
addr = self.adb.get_txin_address(txin)
if self.adb.is_mine(addr):
sh_out = txin.sighash & (Sighash.ANYONECANPAY ^ 0xff)
sh_in = txin.sighash & Sighash.ANYONECANPAY
confirm |= hintmap[sh_out][0] | hintmap[sh_in][0]
reject |= hintmap[sh_out][1] | hintmap[sh_in][1]
for sh in [sh_out, sh_in]:
msg = hintmap[sh][2]
if msg:
messages.append('%s: %s' % (_('Fatal') if hintmap[sh][1] else _('Warning'),
msg.format(txin_idx)))
return confirm, reject, '\n'.join(messages)
def get_tx_fee_warning( def get_tx_fee_warning(
self, *, self, *,
invoice_amt: int, invoice_amt: int,

Loading…
Cancel
Save