@ -256,6 +256,60 @@ class TransactionPotentiallyDangerousException(Exception): pass
class TransactionDangerousException ( TransactionPotentiallyDangerousException ) : 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 ) :
class BumpFeeStrategy ( enum . Enum ) :
PRESERVE_PAYMENT = enum . auto ( )
PRESERVE_PAYMENT = enum . auto ( )
DECREASE_PAYMENT = enum . auto ( )
DECREASE_PAYMENT = enum . auto ( )
@ -2478,21 +2532,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return tx
return tx
# check if signing is dangerous
# check if signing is dangerous
should_confirm , should_reject , message = self . check_sighash ( tx )
sh_danger = self . check_sighash ( tx )
if should_reject :
if sh_danger . needs_reject ( ) :
raise TransactionDangerousException ( ' Not signing transaction: \n ' + message )
raise TransactionDangerousException ( ' Not signing transaction: \n ' + sh_danger . get_long_message ( ) )
if not ignore_warnings :
if sh_danger . needs_confirm ( ) and not ignore_warnings :
if should_confirm :
raise TransactionPotentiallyDangerousException ( ' Not signing transaction: \n ' + sh_danger . get_long_message ( ) )
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
@ -3037,8 +3081,8 @@ class Abstract_Wallet(ABC, Logger, EventListener):
self . sign_transaction ( tx , password )
self . sign_transaction ( tx , password )
return tx
return tx
def get_warning_for _risk_of_burning_coins_as_fees( self , tx : ' PartialTransaction ' ) - > Optional [ str ] :
def _check _risk_of_burning_coins_as_fees( self , tx : ' PartialTransaction ' ) - > TxSighashDanger :
""" Returns a warning message if there is risk of burning coins as fees if we sign.
""" 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 that if not all inputs are ismine , e . g . coinjoin , the risk is not just about fees .
Note :
Note :
@ -3047,59 +3091,77 @@ class Abstract_Wallet(ABC, Logger, EventListener):
- BIP - taproot sighash commits to * all * input amounts
- BIP - taproot sighash commits to * all * input amounts
"""
"""
assert isinstance ( tx , PartialTransaction )
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 we have all full previous txs, we *know* all the input amounts -> fine
if all ( [ txin . utxo for txin in tx . inputs ( ) ] ) :
if all ( [ txin . utxo for txin in tx . inputs ( ) ] ) :
return None
return TxSighashDanger ( risk_level = rl . SAFE )
# a single segwit input -> fine
# a single segwit input -> fine
if len ( tx . inputs ( ) ) == 1 and tx . inputs ( ) [ 0 ] . is_segwit ( ) and tx . inputs ( ) [ 0 ] . witness_utxo :
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
# coinjoin or similar
if any ( [ not self . is_mine ( txin . address ) for txin in tx . inputs ( ) ] ) :
if any ( [ not self . is_mine ( txin . address ) for txin in tx . inputs ( ) ] ) :
return ( _ ( " Warning " ) + " : "
messages = [ ( _ ( " Warning " ) + " : "
+ _ ( " The input amounts could not be verified as the previous transactions are missing. \n "
+ _ ( " The input amounts could not be verified as the previous transactions are missing. \n "
" The amount of money being spent CANNOT be verified. " ) )
" 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
# some inputs are legacy
if any ( [ not txin . is_segwit ( ) for txin in tx . inputs ( ) ] ) :
if any ( [ not txin . is_segwit ( ) for txin in tx . inputs ( ) ] ) :
return ( _ ( " Warning " ) + " : "
messages = [ ( _ ( " Warning " ) + " : "
+ _ ( " The fee could not be verified. Signing non-segwit inputs is risky: \n "
+ _ ( " The fee could not be verified. Signing non-segwit inputs is risky: \n "
" if this transaction was maliciously modified before you sign, \n "
" if this transaction was maliciously modified before you sign, \n "
" you might end up paying a higher mining fee than displayed. " ) )
" 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
# all inputs are segwit
# https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-August/014843.html
# https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-August/014843.html
return ( _ ( " Warning " ) + " : "
messages = [ ( _ ( " Warning " ) + " : "
+ _ ( " If you received this transaction from an untrusted device, "
+ _ ( " If you received this transaction from an untrusted device, "
" 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. " ) ) ]
return TxSighashDanger ( risk_level = rl . FEE_WARNING_SKIPCONFIRM , short_message = short_message , messages = messages )
def check_sighash ( self , tx : ' PartialTransaction ' ) - > ( bool , bool , str ) :
""" Checks the Sighash for my inputs and return hints in the form
def check_sighash ( self , tx : ' PartialTransaction ' ) - > TxSighashDanger :
( confirm , reject , message ) , where confirm indicates the user should explicitly confirm ,
""" Checks the Sighash for my inputs and considers if the tx is safe to sign. """
reject indicates the transaction should be rejected ( unless overruled in e . g . a config property ) ,
assert isinstance ( tx , PartialTransaction )
and message containing a user - facing message describing the context """
rl = TxSighashRiskLevel
hintmap = dict ( [
hintmap = {
( 0 , ( False , False , None ) ) ,
0 : ( rl . SAFE , None ) ,
( Sighash . NONE , ( True , True , _ ( ' Input {} is marked SIGHASH_NONE. ' ) ) ) ,
Sighash . NONE : ( rl . INSANE_SIGHASH , _ ( ' Input {} is marked SIGHASH_NONE. ' ) ) ,
( Sighash . SINGLE , ( True , False , _ ( ' Input {} is marked SIGHASH_SINGLE. ' ) ) ) ,
Sighash . SINGLE : ( rl . WEIRD_SIGHASH , _ ( ' Input {} is marked SIGHASH_SINGLE. ' ) ) ,
( Sighash . ALL , ( False , False , None ) ) ,
Sighash . ALL : ( rl . SAFE , None ) ,
( Sighash . ANYONECANPAY , ( True , False , _ ( ' Input {} is marked SIGHASH_ANYONECANPAY. ' ) ) )
Sighash . ANYONECANPAY : ( rl . WEIRD_SIGHASH , _ ( ' Input {} is marked SIGHASH_ANYONECANPAY. ' ) ) ,
] )
}
confirm = False
sighash_danger = TxSighashDanger ( )
reject = False
messages = [ ]
for txin_idx , txin in enumerate ( tx . inputs ( ) ) :
for txin_idx , txin in enumerate ( tx . inputs ( ) ) :
if txin . sighash and txin . sighash != Sighash . ALL : # non-standard
if txin . sighash in ( None , Sighash . ALL ) :
addr = self . adb . get_txin_address ( txin )
continue # None will get converted to Sighash.ALL, so these values are safe
if self . is_mine ( addr ) :
# found interesting sighash flag
sh_out = txin . sighash & ( Sighash . ANYONECANPAY ^ 0xff )
addr = self . adb . get_txin_address ( txin )
sh_in = txin . sighash & Sighash . ANYONECANPAY
if self . is_mine ( addr ) :
confirm | = hintmap [ sh_out ] [ 0 ] | hintmap [ sh_in ] [ 0 ]
sh_base = txin . sighash & ( Sighash . ANYONECANPAY ^ 0xff )
reject | = hintmap [ sh_out ] [ 1 ] | hintmap [ sh_in ] [ 1 ]
sh_acp = txin . sighash & Sighash . ANYONECANPAY
for sh in [ sh_out , sh_in ] :
for sh in [ sh_base , sh_acp ] :
msg = hintmap [ sh ] [ 2 ]
if msg := hintmap [ sh ] [ 1 ] :
if msg :
risk_level = hintmap [ sh ] [ 0 ]
messages . append ( ' %s : %s ' % ( _ ( ' Fatal ' ) if hintmap [ sh ] [ 1 ] else _ ( ' Warning ' ) ,
header = _ ( ' Fatal ' ) if TxSighashDanger ( risk_level = risk_level ) . needs_reject ( ) else _ ( ' Warning ' )
msg . format ( txin_idx ) ) )
shd = TxSighashDanger (
return confirm , reject , ' \n ' . join ( messages )
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 (
def get_tx_fee_warning (
self , * ,
self , * ,