diff --git a/electrum/constants.py b/electrum/constants.py index 0ea93d319..014106b1e 100644 --- a/electrum/constants.py +++ b/electrum/constants.py @@ -26,7 +26,7 @@ import os import json -from .util import inv_dict +from .util import inv_dict, all_subclasses from . import bitcoin @@ -47,7 +47,17 @@ BIP39_WALLET_FORMATS = read_json('bip39_wallet_formats.json', []) class AbstractNet: - BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS = 0 + NET_NAME: str + TESTNET: bool + WIF_PREFIX: int + ADDRTYPE_P2PKH: int + ADDRTYPE_P2SH: int + SEGWIT_HRP: str + BOLT11_HRP: str + GENESIS: str + BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS: int = 0 + BIP44_COIN_TYPE: int + LN_REALM_BYTE: int @classmethod def max_checkpoint(cls) -> int: @@ -171,6 +181,8 @@ class BitcoinSignet(BitcoinTestnet): LN_DNS_SEEDS = [] +NETS_LIST = tuple(all_subclasses(AbstractNet)) + # don't import net directly, import the module instead (so that net is singleton) net = BitcoinMainnet diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index f3dbb3612..7deb3db63 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -187,7 +187,7 @@ class SendScreen(CScreen, Logger): def set_ln_invoice(self, invoice: str): try: invoice = str(invoice).lower() - lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) + lnaddr = lndecode(invoice) except Exception as e: self.app.show_info(invoice + _(" is not a valid Lightning invoice: ") + repr(e)) # repr because str(Exception()) == '' return diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index f7af50165..f234f0ab2 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1969,7 +1969,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def parse_lightning_invoice(self, invoice): """Parse ln invoice, and prepare the send tab for it.""" try: - lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) + lnaddr = lndecode(invoice) except Exception as e: raise LnDecodeException(e) from e pubkey = bh2u(lnaddr.pubkey.serialize()) @@ -2180,7 +2180,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): d.exec_() def show_lightning_invoice(self, invoice: LNInvoice): - lnaddr = lndecode(invoice.invoice, expected_hrp=constants.net.SEGWIT_HRP) + lnaddr = lndecode(invoice.invoice) d = WindowModalDialog(self, _("Lightning Invoice")) vbox = QVBoxLayout(d) grid = QGridLayout() diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py index 0f2c0ef2a..b678d6410 100644 --- a/electrum/lnaddr.py +++ b/electrum/lnaddr.py @@ -6,7 +6,7 @@ import time from hashlib import sha256 from binascii import hexlify from decimal import Decimal -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Type import random import bitstring @@ -15,6 +15,7 @@ from .bitcoin import hash160_to_b58_address, b58_address_to_hash160, TOTAL_COIN_ from .segwit_addr import bech32_encode, bech32_decode, CHARSET from . import segwit_addr from . import constants +from .constants import AbstractNet from . import ecc from .bitcoin import COIN @@ -83,57 +84,42 @@ def bitarray_to_u5(barr): return ret -def encode_fallback(fallback: str, currency): +def encode_fallback(fallback: str, net: Type[AbstractNet]): """ Encode all supported fallback addresses. """ - if currency in [constants.BitcoinMainnet.SEGWIT_HRP, constants.BitcoinTestnet.SEGWIT_HRP]: - wver, wprog_ints = segwit_addr.decode_segwit_address(currency, fallback) - if wver is not None: - wprog = bytes(wprog_ints) - else: - addrtype, addr = b58_address_to_hash160(fallback) - if is_p2pkh(currency, addrtype): - wver = 17 - elif is_p2sh(currency, addrtype): - wver = 18 - else: - raise ValueError("Unknown address type for {}".format(currency)) - wprog = addr - return tagged('f', bitstring.pack("uint:5", wver) + wprog) + wver, wprog_ints = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, fallback) + if wver is not None: + wprog = bytes(wprog_ints) else: - raise NotImplementedError("Support for currency {} not implemented".format(currency)) - - -def parse_fallback(fallback, currency): - if currency in [constants.BitcoinMainnet.SEGWIT_HRP, constants.BitcoinTestnet.SEGWIT_HRP]: - wver = fallback[0:5].uint - if wver == 17: - addr=hash160_to_b58_address(fallback[5:].tobytes(), base58_prefix_map[currency][0]) - elif wver == 18: - addr=hash160_to_b58_address(fallback[5:].tobytes(), base58_prefix_map[currency][1]) - elif wver <= 16: - witprog = fallback[5:] # cut witver - witprog = witprog[:len(witprog) // 8 * 8] # can only be full bytes - witprog = witprog.tobytes() - addr = segwit_addr.encode_segwit_address(currency, wver, witprog) + addrtype, addr = b58_address_to_hash160(fallback) + if addrtype == net.ADDRTYPE_P2PKH: + wver = 17 + elif addrtype == net.ADDRTYPE_P2SH: + wver = 18 else: - return None + raise ValueError(f"Unknown address type {addrtype} for {net}") + wprog = addr + return tagged('f', bitstring.pack("uint:5", wver) + wprog) + + +def parse_fallback(fallback, net: Type[AbstractNet]): + wver = fallback[0:5].uint + if wver == 17: + addr = hash160_to_b58_address(fallback[5:].tobytes(), net.ADDRTYPE_P2PKH) + elif wver == 18: + addr = hash160_to_b58_address(fallback[5:].tobytes(), net.ADDRTYPE_P2SH) + elif wver <= 16: + witprog = fallback[5:] # cut witver + witprog = witprog[:len(witprog) // 8 * 8] # can only be full bytes + witprog = witprog.tobytes() + addr = segwit_addr.encode_segwit_address(net.SEGWIT_HRP, wver, witprog) else: - addr=fallback.tobytes() + return None return addr -# Map of classical and witness address prefixes -base58_prefix_map = { - constants.BitcoinMainnet.SEGWIT_HRP : (constants.BitcoinMainnet.ADDRTYPE_P2PKH, constants.BitcoinMainnet.ADDRTYPE_P2SH), - constants.BitcoinTestnet.SEGWIT_HRP : (constants.BitcoinTestnet.ADDRTYPE_P2PKH, constants.BitcoinTestnet.ADDRTYPE_P2SH) -} - -def is_p2pkh(currency, prefix): - return prefix == base58_prefix_map[currency][0] +BOLT11_HRP_INV_DICT = {net.BOLT11_HRP: net for net in constants.NETS_LIST} -def is_p2sh(currency, prefix): - return prefix == base58_prefix_map[currency][1] # Tagged field containing BitArray def tagged(char, l): @@ -178,13 +164,10 @@ def pull_tagged(stream): return (CHARSET[tag], stream.read(length * 5), stream) def lnencode(addr: 'LnAddr', privkey) -> str: - # see https://github.com/lightningnetwork/lightning-rfc/pull/844 - if constants.net.NET_NAME == "signet": - addr.currency = constants.net.BOLT11_HRP if addr.amount: - amount = addr.currency + shorten_amount(addr.amount) + amount = addr.net.BOLT11_HRP + shorten_amount(addr.amount) else: - amount = addr.currency if addr.currency else '' + amount = addr.net.BOLT11_HRP if addr.net else '' hrp = 'ln' + amount @@ -221,7 +204,7 @@ def lnencode(addr: 'LnAddr', privkey) -> str: route = bitstring.BitArray(pubkey) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv) data += tagged('t', route) elif k == 'f': - data += encode_fallback(v, addr.currency) + data += encode_fallback(v, addr.net) elif k == 'd': # truncate to max length: 1024*5 bits = 639 bytes data += tagged_bytes('d', v.encode()[0:639]) @@ -270,7 +253,7 @@ def lnencode(addr: 'LnAddr', privkey) -> str: class LnAddr(object): - def __init__(self, *, paymenthash: bytes = None, amount=None, currency=None, tags=None, date=None, + def __init__(self, *, paymenthash: bytes = None, amount=None, net: Type[AbstractNet] = None, tags=None, date=None, payment_secret: bytes = None): self.date = int(time.time()) if not date else int(date) self.tags = [] if not tags else tags @@ -279,7 +262,7 @@ class LnAddr(object): self.payment_secret = payment_secret self.signature = None self.pubkey = None - self.currency = constants.net.SEGWIT_HRP if currency is None else currency + self.net = constants.net if net is None else net # type: Type[AbstractNet] self._amount = amount # type: Optional[Decimal] # in bitcoins self._min_final_cltv_expiry = 18 @@ -330,7 +313,7 @@ class LnAddr(object): def __str__(self): return "LnAddr[{}, amount={}{} tags=[{}]]".format( hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None, - self.amount, self.currency, + self.amount, self.net.BOLT11_HRP, ", ".join([k + '=' + str(v) for k, v in self.tags]) ) @@ -367,12 +350,9 @@ class SerializableKey: def serialize(self): return self.pubkey.get_public_key_bytes(True) -def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr: - if expected_hrp is None: - expected_hrp = constants.net.SEGWIT_HRP - # see https://github.com/lightningnetwork/lightning-rfc/pull/844 - if constants.net.NET_NAME == "signet": - expected_hrp = constants.net.BOLT11_HRP +def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr: + if net is None: + net = constants.net decoded_bech32 = bech32_decode(invoice, ignore_long_length=True) hrp = decoded_bech32.hrp data = decoded_bech32.data @@ -387,8 +367,8 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr: if not hrp.startswith('ln'): raise ValueError("Does not start with ln") - if not hrp[2:].startswith(expected_hrp): - raise ValueError("Wrong Lightning invoice HRP " + hrp[2:] + ", should be " + expected_hrp) + if not hrp[2:].startswith(net.BOLT11_HRP): + raise ValueError(f"Wrong Lightning invoice HRP {hrp[2:]}, should be {net.BOLT11_HRP}") data = u5_to_bitarray(data) @@ -403,7 +383,7 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr: m = re.search("[^\\d]+", hrp[2:]) if m: - addr.currency = m.group(0) + addr.net = BOLT11_HRP_INV_DICT[m.group(0)] amountstr = hrp[2+m.end():] # BOLT #11: # @@ -453,7 +433,7 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr: s.read(16).uintbe) addr.tags.append(('t', e)) elif tag == 'f': - fallback = parse_fallback(tagdata, addr.currency) + fallback = parse_fallback(tagdata, addr.net) if fallback: addr.tags.append(('f', fallback)) else: @@ -532,13 +512,3 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr: addr.pubkey = SerializableKey(ecc.ECPubkey.from_sig_string(sigdecoded[:64], sigdecoded[64], hrp_hash)) return addr - - - - -if __name__ == '__main__': - # run using - # python3 -m electrum.lnaddr - # python3 -m electrum.lnaddr lntb1n1pdlcakepp5e7rn0knl0gm46qqp9eqdsza2c942d8pjqnwa5903n39zu28sgk3sdq423jhxapqv3hkuct5d9hkucqp2rzjqwyx8nu2hygyvgc02cwdtvuxe0lcxz06qt3lpsldzcdr46my5epmj9vk9sqqqlcqqqqqqqlgqqqqqqgqjqdhnmkgahfaynuhe9md8k49xhxuatnv6jckfmsjq8maxta2l0trh5sdrqlyjlwutdnpd5gwmdnyytsl9q0dj6g08jacvthtpeg383k0sq542rz2 tb1n - import sys - print(lndecode(sys.argv[1], expected_hrp=sys.argv[2])) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index fda782079..a926ad618 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1398,7 +1398,7 @@ class LNWallet(LNWorker): @staticmethod def _check_invoice(invoice: str, *, amount_msat: int = None) -> LnAddr: - addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) + addr = lndecode(invoice) if addr.is_expired(): raise InvoiceError(_("This invoice has expired")) if amount_msat: # replace amt in invoice. main usecase is paying zero amt invoices diff --git a/electrum/tests/test_bolt11.py b/electrum/tests/test_bolt11.py index 4841eb62b..1b33eec31 100644 --- a/electrum/tests/test_bolt11.py +++ b/electrum/tests/test_bolt11.py @@ -8,6 +8,7 @@ from electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, from electrum.segwit_addr import bech32_encode, bech32_decode from electrum import segwit_addr from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage, LnFeatures +from electrum import constants from . import ElectrumTestCase @@ -69,7 +70,7 @@ class TestBolt11(ElectrumTestCase): "lnbc1m1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9rflz25dx0qw6kdg05u0c5hdc30yq6ga6ew4pz86n244va45nchns9zrs3wjxznsqnt37hz7pswvc56wvuhxcjyd6k3lqf4ujynyxuspmvr078"), (LnAddr(date=timestamp, paymenthash=RHASH, amount=Decimal('1'), tags=[('h', longdescription)]), "lnbc11ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs2qjafckq94q3js6lvqz2kmenn9ysjejyj8fm4hlx0xtqhaxfzlxjappkgp0hmm40dnuan4v3jy83lqjup2n0fdzgysg049y9l9uc98qq07kfd3"), - (LnAddr(date=timestamp, paymenthash=RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]), + (LnAddr(date=timestamp, paymenthash=RHASH, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]), "lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un98hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsr9zktgu78k8p9t8555ve37qwfvqn6ga37fnfwhgexmf20nzdpmuhwvuv7zra3xrh8y2ggxxuemqfsgka9x7uzsrcx8rfv85c8pmhq9gq4sampn"), (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[ ('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3), @@ -109,7 +110,7 @@ class TestBolt11(ElectrumTestCase): for lnaddr1, invoice_str1 in tests: invoice_str2 = lnencode(lnaddr1, PRIVKEY) self.assertEqual(invoice_str1, invoice_str2) - lnaddr2 = lndecode(invoice_str2, expected_hrp=lnaddr1.currency) + lnaddr2 = lndecode(invoice_str2, net=lnaddr1.net) self.compare(lnaddr1, lnaddr2) def test_n_decoding(self): @@ -134,11 +135,11 @@ class TestBolt11(ElectrumTestCase): def test_min_final_cltv_expiry_decoding(self): lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe", - expected_hrp="sb") + net=constants.BitcoinSimnet) self.assertEqual(144, lnaddr.get_min_final_cltv_expiry()) lnaddr = lndecode("lntb15u1p0m6lzupp5zqjthgvaad9mewmdjuehwddyze9d8zyxcc43zhaddeegt37sndgsdq4xysyymr0vd4kzcmrd9hx7cqp7xqrrss9qy9qsqsp5vlhcs24hwm747w8f3uau2tlrdkvjaglffnsstwyamj84cxuhrn2s8tut3jqumepu42azyyjpgqa4w9w03204zp9h4clk499y2umstl6s29hqyj8vv4as6zt5567ux7l3f66m8pjhk65zjaq2esezk7ll2kcpljewkg", - expected_hrp="tb") + net=constants.BitcoinTestnet) self.assertEqual(30, lnaddr.get_min_final_cltv_expiry()) def test_min_final_cltv_expiry_roundtrip(self): diff --git a/electrum/util.py b/electrum/util.py index 12b69a82d..d542cbe86 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -24,7 +24,7 @@ import binascii import os, sys, re, json from collections import defaultdict, OrderedDict from typing import (NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, - Sequence, Dict, Generic, TypeVar, List, Iterable) + Sequence, Dict, Generic, TypeVar, List, Iterable, Set) from datetime import datetime import decimal from decimal import Decimal @@ -72,6 +72,14 @@ def inv_dict(d): return {v: k for k, v in d.items()} +def all_subclasses(cls) -> Set: + """Return all (transitive) subclasses of cls.""" + res = set(cls.__subclasses__()) + for sub in res.copy(): + res |= all_subclasses(sub) + return res + + ca_path = certifi.where()