Browse Source

invoices: also run amount-validator on setter

- @amount_msat.validator prevents the creation of invoices with e.g. too large amounts
- however the qml gui is mutating invoices by directly setting the `amount_msat` field,
  and it looks like attrs validators only run during init.
  We can use `on_setattr` (introduced in attrs==20.1.0).
- a wallet db upgrade is added to rm existing insane invoices
- btw the qml gui was already doing its own input validation on the textedit
  (see qeconfig.btcAmountRegex). however that only limits the input to not have more
  chars than what is needed to represent 21M BTC (e.g. you can still enter 99M BTC,
  which the invoice logic does not tolerate later on - but is normally caught).

fixes https://github.com/spesmilo/electrum/issues/8582
master
SomberNight 2 years ago
parent
commit
4e6e6f76ca
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 2
      contrib/requirements/requirements.txt
  2. 14
      electrum/invoices.py
  3. 40
      electrum/tests/test_invoices.py
  4. 20
      electrum/wallet_db.py

2
contrib/requirements/requirements.txt

@ -6,7 +6,7 @@ aiohttp>=3.3.0,<4.0.0
aiohttp_socks>=0.3
certifi
bitstring
attrs>=19.2.0
attrs>=20.1.0
# Note that we also need the dnspython[DNSSEC] extra which pulls in cryptography,
# but as that is not pure-python it cannot be listed in this file!

14
electrum/invoices.py

@ -94,13 +94,18 @@ class BaseInvoice(StoredObject):
"""
Base class for Invoice and Request
In the code, we use 'invoice' for outgoing payments, and 'request' for incoming payments.
TODO this class is getting too complicated for "attrs"... maybe we should rewrite it without.
"""
# mandatory fields
amount_msat = attr.ib(kw_only=True) # type: Optional[Union[int, str]] # can be '!' or None
amount_msat = attr.ib( # can be '!' or None
kw_only=True, on_setattr=attr.setters.validate) # type: Optional[Union[int, str]]
message = attr.ib(type=str, kw_only=True)
time = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int)) # timestamp of the invoice
exp = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int)) # expiration delay (relative). 0 means never
time = attr.ib( # timestamp of the invoice
type=int, kw_only=True, validator=attr.validators.instance_of(int), on_setattr=attr.setters.validate)
exp = attr.ib( # expiration delay (relative). 0 means never
type=int, kw_only=True, validator=attr.validators.instance_of(int), on_setattr=attr.setters.validate)
# optional fields.
# an request (incoming) can be satisfied onchain, using lightning or using a swap
@ -108,7 +113,8 @@ class BaseInvoice(StoredObject):
# onchain only
outputs = attr.ib(kw_only=True, converter=_decode_outputs) # type: Optional[List[PartialTxOutput]]
height = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int)) # only for receiving
height = attr.ib( # only for receiving
type=int, kw_only=True, validator=attr.validators.instance_of(int), on_setattr=attr.setters.validate)
bip70 = attr.ib(type=str, kw_only=True) # type: Optional[str]
#bip70_requestor = attr.ib(type=str, kw_only=True) # type: Optional[str]

40
electrum/tests/test_invoices.py

@ -5,10 +5,10 @@ from . import ElectrumTestCase
from electrum.simple_config import SimpleConfig
from electrum.wallet import restore_wallet_from_text, Standard_Wallet, Abstract_Wallet
from electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, BaseInvoice
from electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, BaseInvoice, Invoice, LN_EXPIRY_NEVER
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED
from electrum.transaction import Transaction, PartialTxOutput
from electrum.util import TxMinedInfo
from electrum.util import TxMinedInfo, InvoiceError
class TestWalletPaymentRequests(ElectrumTestCase):
@ -266,3 +266,39 @@ class TestWalletPaymentRequests(ElectrumTestCase):
BaseInvoice._get_cur_time = lambda *args: time.time() + 200_000
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2))
self.assertEqual(pr2, wallet1.get_request_by_addr(addr1))
class TestBaseInvoice(ElectrumTestCase):
TESTNET = True
async def test_arg_validation(self):
amount_sat = 10_000
outputs = [PartialTxOutput.from_address_and_value("tb1qmjzmg8nd4z56ar4fpngzsr6euktrhnjg9td385", amount_sat)]
invoice = Invoice(
amount_msat=amount_sat * 1000,
message="mymsg",
time=1692716965,
exp=LN_EXPIRY_NEVER,
outputs=outputs,
bip70=None,
height=0,
lightning_invoice=None,
)
with self.assertRaises(InvoiceError):
invoice.amount_msat = 10**20
with self.assertRaises(InvoiceError):
invoice2 = Invoice(
amount_msat=10**20,
message="mymsg",
time=1692716965,
exp=LN_EXPIRY_NEVER,
outputs=outputs,
bip70=None,
height=0,
lightning_invoice=None,
)
with self.assertRaises(TypeError):
invoice.time = "asd"
with self.assertRaises(TypeError):
invoice.exp = "asd"

20
electrum/wallet_db.py

@ -54,7 +54,7 @@ from .version import ELECTRUM_VERSION
OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 53 # electrum >= 2.7 will set this to prevent
FINAL_SEED_VERSION = 54 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format
@ -239,6 +239,7 @@ class WalletDB(JsonDB):
self._convert_version_51()
self._convert_version_52()
self._convert_version_53()
self._convert_version_54()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks()
@ -1076,6 +1077,23 @@ class WalletDB(JsonDB):
cb['local_payment_pubkey'] = None
self.data['seed_version'] = 53
def _convert_version_54(self):
# note: similar to convert_version_38
if not self._is_upgrade_method_needed(53, 53):
return
from .bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN
max_sats = TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN
requests = self.data.get('payment_requests', {})
invoices = self.data.get('invoices', {})
for d in [invoices, requests]:
for key, item in list(d.items()):
amount_msat = item['amount_msat']
if amount_msat == '!':
continue
if not (isinstance(amount_msat, int) and 0 <= amount_msat <= max_sats * 1000):
del d[key]
self.data['seed_version'] = 54
def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return

Loading…
Cancel
Save