@ -256,6 +256,60 @@ class TransactionPotentiallyDangerousException(Exception): pass
class TransactionDangerousException ( TransactionPotentiallyDangerousException ) : pass
class TxSighashRiskLevel ( enum . IntEnum ) :
# higher value -> more risk
SAFE = 0
FEE_WARNING_SKIPCONFIRM = 1 # show warning icon (ignored for CLI)
FEE_WARNING_NEEDCONFIRM = 2 # prompt user for confirmation
WEIRD_SIGHASH = 3 # prompt user for confirmation
INSANE_SIGHASH = 4 # reject
class TxSighashDanger :
def __init__ (
self ,
* ,
risk_level : TxSighashRiskLevel = TxSighashRiskLevel . SAFE ,
short_message : str = None ,
messages : List [ str ] = None ,
) :
self . risk_level = risk_level
self . short_message = short_message
self . _messages = messages or [ ]
def needs_confirm ( self ) - > bool :
""" If True, the user should be prompted for explicit confirmation before signing. """
return self . risk_level > = TxSighashRiskLevel . FEE_WARNING_NEEDCONFIRM
def needs_reject ( self ) - > bool :
""" If True, the transaction should be rejected, i.e. abort signing. """
return self . risk_level > = TxSighashRiskLevel . INSANE_SIGHASH
def get_long_message ( self ) - > str :
""" Returns a description of the potential dangers of signing the tx that can be shown to the user.
Empty string if there are none .
"""
if self . short_message :
header = [ self . short_message ]
else :
header = [ ]
return " \n " . join ( header + self . _messages )
def combine ( * args : ' TxSighashDanger ' ) - > ' TxSighashDanger ' :
max_danger = max ( args , key = lambda sighash_danger : sighash_danger . risk_level ) # type: TxSighashDanger
messages = [ msg for sighash_danger in args for msg in sighash_danger . _messages ]
return TxSighashDanger (
risk_level = max_danger . risk_level ,
short_message = max_danger . short_message ,
messages = messages ,
)
def __repr__ ( self ) :
return ( f " < { self . __class__ . __name__ } risk_level= { self . risk_level } "
f " short_message= { self . short_message !r} _messages= { self . _messages !r} > " )
class BumpFeeStrategy ( enum . Enum ) :
PRESERVE_PAYMENT = enum . auto ( )
DECREASE_PAYMENT = enum . auto ( )
@ -2478,21 +2532,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
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 )
sh_danger = self . check_sighash ( tx )
if sh_danger . needs_reject ( ) :
raise TransactionDangerousException ( ' Not signing transaction: \n ' + sh_danger . get_long_message ( ) )
if sh_danger . needs_confirm ( ) and not ignore_warnings :
raise TransactionPotentiallyDangerousException ( ' Not signing transaction: \n ' + sh_danger . get_long_message ( ) )
# add info to a temporary tx copy; including xpubs
# and full derivation paths as hw keystores might want them
@ -3037,8 +3081,8 @@ class Abstract_Wallet(ABC, Logger, EventListener):
self . sign_transaction ( tx , password )
return tx
def get_warning_for _risk_of_burning_coins_as_fees( self , tx : ' PartialTransaction ' ) - > Optional [ str ] :
""" Returns a warning message if there is risk of burning coins as fees if we sign.
def _check _risk_of_burning_coins_as_fees( self , tx : ' PartialTransaction ' ) - > TxSighashDanger :
""" Helper method to check if there is risk of burning coins as fees if we sign.
Note that if not all inputs are ismine , e . g . coinjoin , the risk is not just about fees .
Note :
@ -3047,59 +3091,77 @@ class Abstract_Wallet(ABC, Logger, EventListener):
- BIP - taproot sighash commits to * all * input amounts
"""
assert isinstance ( tx , PartialTransaction )
rl = TxSighashRiskLevel
short_message = _ ( " Warning " ) + " : " + _ ( " The fee could not be verified! " )
# check that all inputs use SIGHASH_ALL
if not all ( txin . sighash in ( None , Sighash . ALL ) for txin in tx . inputs ( ) ) :
messages = [ ( _ ( " Warning " ) + " : "
+ _ ( " Some inputs use non-default sighash flags, which might affect the fee. " ) ) ]
return TxSighashDanger ( risk_level = rl . FEE_WARNING_NEEDCONFIRM , short_message = short_message , messages = messages )
# if we have all full previous txs, we *know* all the input amounts -> fine
if all ( [ txin . utxo for txin in tx . inputs ( ) ] ) :
return None
return TxSighashDanger ( risk_level = rl . SAFE )
# a single segwit input -> fine
if len ( tx . inputs ( ) ) == 1 and tx . inputs ( ) [ 0 ] . is_segwit ( ) and tx . inputs ( ) [ 0 ] . witness_utxo :
return None
return TxSighashDanger ( risk_level = rl . SAFE )
# coinjoin or similar
if any ( [ not self . is_mine ( txin . address ) for txin in tx . inputs ( ) ] ) :
return ( _ ( " Warning " ) + " : "
+ _ ( " The input amounts could not be verified as the previous transactions are missing. \n "
" The amount of money being spent CANNOT be verified. " ) )
messages = [ ( _ ( " Warning " ) + " : "
+ _ ( " The input amounts could not be verified as the previous transactions are missing. \n "
" The amount of money being spent CANNOT be verified. " ) ) ]
return TxSighashDanger ( risk_level = rl . FEE_WARNING_NEEDCONFIRM , short_message = short_message , messages = messages )
# some inputs are legacy
if any ( [ not txin . is_segwit ( ) for txin in tx . inputs ( ) ] ) :
return ( _ ( " Warning " ) + " : "
+ _ ( " The fee could not be verified. Signing non-segwit inputs is risky: \n "
" if this transaction was maliciously modified before you sign, \n "
" you might end up paying a higher mining fee than displayed. " ) )
messages = [ ( _ ( " Warning " ) + " : "
+ _ ( " The fee could not be verified. Signing non-segwit inputs is risky: \n "
" if this transaction was maliciously modified before you sign, \n "
" you might end up paying a higher mining fee than displayed. " ) ) ]
return TxSighashDanger ( risk_level = rl . FEE_WARNING_NEEDCONFIRM , short_message = short_message , messages = messages )
# all inputs are segwit
# https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-August/014843.html
return ( _ ( " Warning " ) + " : "
+ _ ( " If you received this transaction from an untrusted device, "
" do not accept to sign it more than once, \n "
" 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 = [ ]
messages = [ ( _ ( " Warning " ) + " : "
+ _ ( " If you received this transaction from an untrusted device, "
" do not accept to sign it more than once, \n "
" otherwise you could end up paying a different fee. " ) ) ]
return TxSighashDanger ( risk_level = rl . FEE_WARNING_SKIPCONFIRM , short_message = short_message , messages = messages )
def check_sighash ( self , tx : ' PartialTransaction ' ) - > TxSighashDanger :
""" Checks the Sighash for my inputs and considers if the tx is safe to sign. """
assert isinstance ( tx , PartialTransaction )
rl = TxSighashRiskLevel
hintmap = {
0 : ( rl . SAFE , None ) ,
Sighash . NONE : ( rl . INSANE_SIGHASH , _ ( ' Input {} is marked SIGHASH_NONE. ' ) ) ,
Sighash . SINGLE : ( rl . WEIRD_SIGHASH , _ ( ' Input {} is marked SIGHASH_SINGLE. ' ) ) ,
Sighash . ALL : ( rl . SAFE , None ) ,
Sighash . ANYONECANPAY : ( rl . WEIRD_SIGHASH , _ ( ' Input {} is marked SIGHASH_ANYONECANPAY. ' ) ) ,
}
sighash_danger = TxSighashDanger ( )
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 . 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 )
if txin . sighash in ( None , Sighash . ALL ) :
continue # None will get converted to Sighash.ALL, so these values are safe
# found interesting sighash flag
addr = self . adb . get_txin_address ( txin )
if self . is_mine ( addr ) :
sh_base = txin . sighash & ( Sighash . ANYONECANPAY ^ 0xff )
sh_acp = txin . sighash & Sighash . ANYONECANPAY
for sh in [ sh_base , sh_acp ] :
if msg := hintmap [ sh ] [ 1 ] :
risk_level = hintmap [ sh ] [ 0 ]
header = _ ( ' Fatal ' ) if TxSighashDanger ( risk_level = risk_level ) . needs_reject ( ) else _ ( ' Warning ' )
shd = TxSighashDanger (
risk_level = risk_level ,
short_message = _ ( ' Danger! This transaction uses non-default sighash flags! ' ) ,
messages = [ f " { header } : { msg . format ( txin_idx ) } " ] ,
)
sighash_danger = sighash_danger . combine ( shd )
if sighash_danger . needs_reject ( ) : # no point for further tests
return sighash_danger
# if we show any fee to the user, check now how reliable that is:
if self . get_wallet_delta ( tx ) . fee is not None :
shd = self . _check_risk_of_burning_coins_as_fees ( tx )
sighash_danger = sighash_danger . combine ( shd )
return sighash_danger
def get_tx_fee_warning (
self , * ,