Merge pull request #7041 from SomberNight/20200218_invoice_amt_oob
invoices: validate 'amount' not to be out-of-bounds
This commit is contained in:
@@ -9,7 +9,7 @@ from .i18n import _
|
|||||||
from .util import age
|
from .util import age
|
||||||
from .lnaddr import lndecode, LnAddr
|
from .lnaddr import lndecode, LnAddr
|
||||||
from . import constants
|
from . import constants
|
||||||
from .bitcoin import COIN
|
from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
|
||||||
from .transaction import PartialTxOutput
|
from .transaction import PartialTxOutput
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -130,6 +130,17 @@ class OnchainInvoice(Invoice):
|
|||||||
def get_amount_sat(self) -> Union[int, str]:
|
def get_amount_sat(self) -> Union[int, str]:
|
||||||
return self.amount_sat or 0
|
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
|
@classmethod
|
||||||
def from_bip70_payreq(cls, pr: 'PaymentRequest', height:int) -> 'OnchainInvoice':
|
def from_bip70_payreq(cls, pr: 'PaymentRequest', height:int) -> 'OnchainInvoice':
|
||||||
return OnchainInvoice(
|
return OnchainInvoice(
|
||||||
@@ -153,9 +164,19 @@ class LNInvoice(Invoice):
|
|||||||
__lnaddr = None
|
__lnaddr = None
|
||||||
|
|
||||||
@invoice.validator
|
@invoice.validator
|
||||||
def check(self, attribute, value):
|
def _validate_invoice_str(self, attribute, value):
|
||||||
lndecode(value) # this checks the str can be decoded
|
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
|
@property
|
||||||
def _lnaddr(self) -> LnAddr:
|
def _lnaddr(self) -> LnAddr:
|
||||||
if self.__lnaddr is None:
|
if self.__lnaddr is None:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from typing import Optional
|
|||||||
import random
|
import random
|
||||||
import bitstring
|
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 .segwit_addr import bech32_encode, bech32_decode, CHARSET
|
||||||
from . import constants
|
from . import constants
|
||||||
from . import ecc
|
from . import ecc
|
||||||
@@ -175,13 +175,7 @@ def pull_tagged(stream):
|
|||||||
|
|
||||||
def lnencode(addr: 'LnAddr', privkey) -> str:
|
def lnencode(addr: 'LnAddr', privkey) -> str:
|
||||||
if addr.amount:
|
if addr.amount:
|
||||||
amount = Decimal(str(addr.amount))
|
amount = addr.currency + shorten_amount(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)
|
|
||||||
else:
|
else:
|
||||||
amount = addr.currency if addr.currency else ''
|
amount = addr.currency if addr.currency else ''
|
||||||
|
|
||||||
@@ -278,9 +272,28 @@ class LnAddr(object):
|
|||||||
self.signature = None
|
self.signature = None
|
||||||
self.pubkey = None
|
self.pubkey = None
|
||||||
self.currency = constants.net.SEGWIT_HRP if currency is None else currency
|
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
|
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]:
|
def get_amount_sat(self) -> Optional[Decimal]:
|
||||||
# note that this has msat resolution potentially
|
# note that this has msat resolution potentially
|
||||||
if self.amount is None:
|
if self.amount is None:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
||||||
NEW_SEED_VERSION = 11 # 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
|
# old versions from overwriting new format
|
||||||
|
|
||||||
|
|
||||||
@@ -185,6 +185,7 @@ class WalletDB(JsonDB):
|
|||||||
self._convert_version_35()
|
self._convert_version_35()
|
||||||
self._convert_version_36()
|
self._convert_version_36()
|
||||||
self._convert_version_37()
|
self._convert_version_37()
|
||||||
|
self._convert_version_38()
|
||||||
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
||||||
|
|
||||||
self._after_upgrade_tasks()
|
self._after_upgrade_tasks()
|
||||||
@@ -752,6 +753,31 @@ class WalletDB(JsonDB):
|
|||||||
self.data['lightning_payments'] = payments
|
self.data['lightning_payments'] = payments
|
||||||
self.data['seed_version'] = 37
|
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):
|
def _convert_imported(self):
|
||||||
if not self._is_upgrade_method_needed(0, 13):
|
if not self._is_upgrade_method_needed(0, 13):
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user