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