tMerge pull request #7041 from SomberNight/20200218_invoice_amt_oob - electrum - Electrum Bitcoin wallet (HTM) git clone https://git.parazyd.org/electrum (DIR) Log (DIR) Files (DIR) Refs (DIR) Submodules --- (DIR) commit 998f41256fc4e41ceb0a3a18681e305415a106ef (DIR) parent ba5e73d978a6a79432e1cca6cec431d0c2ca65c8 (HTM) Author: ThomasV <thomasv@electrum.org> Date: Thu, 18 Feb 2021 12:01:48 +0100 Merge pull request #7041 from SomberNight/20200218_invoice_amt_oob invoices: validate 'amount' not to be out-of-bounds Diffstat: M electrum/invoices.py | 25 +++++++++++++++++++++++-- M electrum/lnaddr.py | 31 ++++++++++++++++++++++--------- M electrum/wallet_db.py | 28 +++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 12 deletions(-) --- (DIR) diff --git a/electrum/invoices.py b/electrum/invoices.py t@@ -9,7 +9,7 @@ from .i18n import _ from .util import age from .lnaddr import lndecode, LnAddr from . import constants -from .bitcoin import COIN +from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC from .transaction import PartialTxOutput if TYPE_CHECKING: t@@ -130,6 +130,17 @@ class OnchainInvoice(Invoice): def get_amount_sat(self) -> Union[int, str]: return self.amount_sat or 0 + @amount_sat.validator + def _validate_amount(self, attribute, value): + if isinstance(value, int): + if not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN): + raise ValueError(f"amount is out-of-bounds: {value!r} sat") + elif isinstance(value, str): + if value != "!": + raise ValueError(f"unexpected amount: {value!r}") + else: + raise ValueError(f"unexpected amount: {value!r}") + @classmethod def from_bip70_payreq(cls, pr: 'PaymentRequest', height:int) -> 'OnchainInvoice': return OnchainInvoice( t@@ -153,9 +164,19 @@ class LNInvoice(Invoice): __lnaddr = None @invoice.validator - def check(self, attribute, value): + def _validate_invoice_str(self, attribute, value): lndecode(value) # this checks the str can be decoded + @amount_msat.validator + def _validate_amount(self, attribute, value): + if value is None: + return + if isinstance(value, int): + if not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN * 1000): + raise ValueError(f"amount is out-of-bounds: {value!r} msat") + else: + raise ValueError(f"unexpected amount: {value!r}") + @property def _lnaddr(self) -> LnAddr: if self.__lnaddr is None: (DIR) diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py t@@ -11,7 +11,7 @@ from typing import Optional import random import bitstring -from .bitcoin import hash160_to_b58_address, b58_address_to_hash160 +from .bitcoin import hash160_to_b58_address, b58_address_to_hash160, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC from .segwit_addr import bech32_encode, bech32_decode, CHARSET from . import constants from . import ecc t@@ -175,13 +175,7 @@ def pull_tagged(stream): def lnencode(addr: 'LnAddr', privkey) -> str: if addr.amount: - amount = Decimal(str(addr.amount)) - # We can only send down to millisatoshi. - if amount * 10**12 % 10: - raise ValueError("Cannot encode {}: too many decimal places".format( - addr.amount)) - - amount = addr.currency + shorten_amount(amount) + amount = addr.currency + shorten_amount(addr.amount) else: amount = addr.currency if addr.currency else '' t@@ -278,9 +272,28 @@ class LnAddr(object): self.signature = None self.pubkey = None self.currency = constants.net.SEGWIT_HRP if currency is None else currency - self.amount = amount # type: Optional[Decimal] # in bitcoins + self._amount = amount # type: Optional[Decimal] # in bitcoins self._min_final_cltv_expiry = 18 + @property + def amount(self) -> Optional[Decimal]: + return self._amount + + @amount.setter + def amount(self, value): + if not (isinstance(value, Decimal) or value is None): + raise ValueError(f"amount must be Decimal or None, not {value!r}") + if value is None: + self._amount = None + return + assert isinstance(value, Decimal) + if value.is_nan() or not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC): + raise ValueError(f"amount is out-of-bounds: {value!r} BTC") + if value * 10**12 % 10: + # max resolution is millisatoshi + raise ValueError(f"Cannot encode {value!r}: too many decimal places") + self._amount = value + def get_amount_sat(self) -> Optional[Decimal]: # note that this has msat resolution potentially if self.amount is None: (DIR) diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py t@@ -52,7 +52,7 @@ if TYPE_CHECKING: OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 37 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 38 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format t@@ -185,6 +185,7 @@ class WalletDB(JsonDB): self._convert_version_35() self._convert_version_36() self._convert_version_37() + self._convert_version_38() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self._after_upgrade_tasks() t@@ -752,6 +753,31 @@ class WalletDB(JsonDB): self.data['lightning_payments'] = payments self.data['seed_version'] = 37 + def _convert_version_38(self): + if not self._is_upgrade_method_needed(37, 37): + return + PR_TYPE_ONCHAIN = 0 + PR_TYPE_LN = 2 + 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()): + if item['type'] == PR_TYPE_ONCHAIN: + amount_sat = item['amount_sat'] + if amount_sat == '!': + continue + if not (isinstance(amount_sat, int) and 0 <= amount_sat <= max_sats): + del d[key] + elif item['type'] == PR_TYPE_LN: + amount_msat = item['amount_msat'] + if not amount_msat: + continue + if not (isinstance(amount_msat, int) and 0 <= amount_msat <= max_sats * 1000): + del d[key] + self.data['seed_version'] = 38 + def _convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return