Browse Source

dependencies: remove bitstring

- `bitstring` started depending on `bitarray` in version 4.1 [0]
  - that would mean one additional dependency for us (from yet another maintainer), which is not even pure python
- we only use bitstring for bolt11-parsing
- hence this PR rewrites the bolt11-parsing and removes `bitstring` as dependency
- note: I benchmarked lndecode using [1], and the new code performs better,
  taking around 80% time needed for old code (when using bitstring 3.1.9, pure python).
  Though the variance is quite large in both cases.

[0]: 95ee533ee4/release_notes.txt (L108)
[1]: d7597d96d0
master
SomberNight 2 years ago
parent
commit
cf2ed509b4
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 2
      contrib/deterministic-build/requirements.txt
  2. 1
      contrib/requirements/requirements.txt
  3. 275
      electrum/lnaddr.py
  4. 8
      electrum/segwit_addr.py
  5. 52
      electrum/trampoline.py
  6. 16
      tests/test_bolt11.py

2
contrib/deterministic-build/requirements.txt

@ -10,8 +10,6 @@ async-timeout==4.0.3 \
--hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f --hash=sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f
attrs==22.1.0 \ attrs==22.1.0 \
--hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6
bitstring==3.1.9 \
--hash=sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7
certifi==2024.2.2 \ certifi==2024.2.2 \
--hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f
dnspython==2.2.1 \ dnspython==2.2.1 \

1
contrib/requirements/requirements.txt

@ -5,7 +5,6 @@ aiorpcx>=0.22.0,<0.24
aiohttp>=3.3.0,<4.0.0 aiohttp>=3.3.0,<4.0.0
aiohttp_socks>=0.8.4 aiohttp_socks>=0.8.4
certifi certifi
bitstring
attrs>=20.1.0 attrs>=20.1.0
jsonpatch jsonpatch

275
electrum/lnaddr.py

@ -1,18 +1,17 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
# This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23 # This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23
import io
import re import re
import time import time
from hashlib import sha256 from hashlib import sha256
from binascii import hexlify from binascii import hexlify
from decimal import Decimal from decimal import Decimal
from typing import Optional, TYPE_CHECKING, Type, Dict, Any from typing import Optional, TYPE_CHECKING, Type, Dict, Any, Union, Sequence, List, Tuple
import random import random
import bitstring
from .bitcoin import hash160_to_b58_address, b58_address_to_hash160, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC 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, CHARSET_INVERSE, convertbits
from . import segwit_addr from . import segwit_addr
from . import constants from . import constants
from .constants import AbstractNet from .constants import AbstractNet
@ -75,25 +74,9 @@ def unshorten_amount(amount) -> Decimal:
else: else:
return Decimal(amount) return Decimal(amount)
_INT_TO_BINSTR = {a: '0' * (5-len(bin(a)[2:])) + bin(a)[2:] for a in range(32)}
# Bech32 spits out array of 5-bit values. Shim here.
def u5_to_bitarray(arr):
b = ''.join(_INT_TO_BINSTR[a] for a in arr)
return bitstring.BitArray(bin=b)
def bitarray_to_u5(barr):
assert barr.len % 5 == 0
ret = []
s = bitstring.ConstBitStream(barr)
while s.pos != s.len:
ret.append(s.read(5).uint)
return ret
def encode_fallback(fallback: str, net: Type[AbstractNet]): def encode_fallback_addr(fallback: str, net: Type[AbstractNet]) -> Sequence[int]:
""" Encode all supported fallback addresses. """Encode all supported fallback addresses."""
"""
wver, wprog_ints = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, fallback) wver, wprog_ints = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, fallback)
if wver is not None: if wver is not None:
wprog = bytes(wprog_ints) wprog = bytes(wprog_ints)
@ -106,20 +89,20 @@ def encode_fallback(fallback: str, net: Type[AbstractNet]):
else: else:
raise LnEncodeException(f"Unknown address type {addrtype} for {net}") raise LnEncodeException(f"Unknown address type {addrtype} for {net}")
wprog = addr wprog = addr
return tagged('f', bitstring.pack("uint:5", wver) + wprog) data5 = convertbits(wprog, 8, 5)
assert data5 is not None
return tagged5('f', [wver] + list(data5))
def parse_fallback(fallback, net: Type[AbstractNet]): def parse_fallback_addr(data5: Sequence[int], net: Type[AbstractNet]) -> Optional[str]:
wver = fallback[0:5].uint wver = data5[0]
data8 = bytes(convertbits(data5[1:], 5, 8, False))
if wver == 17: if wver == 17:
addr = hash160_to_b58_address(fallback[5:].tobytes(), net.ADDRTYPE_P2PKH) addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2PKH)
elif wver == 18: elif wver == 18:
addr = hash160_to_b58_address(fallback[5:].tobytes(), net.ADDRTYPE_P2SH) addr = hash160_to_b58_address(data8, net.ADDRTYPE_P2SH)
elif wver <= 16: elif wver <= 16:
witprog = fallback[5:] # cut witver addr = segwit_addr.encode_segwit_address(net.SEGWIT_HRP, wver, data8)
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: else:
return None return None
return addr return addr
@ -128,47 +111,52 @@ def parse_fallback(fallback, net: Type[AbstractNet]):
BOLT11_HRP_INV_DICT = {net.BOLT11_HRP: net for net in constants.NETS_LIST} BOLT11_HRP_INV_DICT = {net.BOLT11_HRP: net for net in constants.NETS_LIST}
# Tagged field containing BitArray def tagged5(char: str, data5: Sequence[int]) -> Sequence[int]:
def tagged(char, l): assert len(data5) < (1 << 10)
# Tagged fields need to be zero-padded to 5 bits. return [CHARSET_INVERSE[char], len(data5) >> 5, len(data5) & 31] + data5
while l.len % 5 != 0:
l.append('0b0')
return bitstring.pack("uint:5, uint:5, uint:5", def tagged8(char: str, data8: Sequence[int]) -> Sequence[int]:
CHARSET.find(char), return tagged5(char, convertbits(data8, 8, 5))
(l.len / 5) / 32, (l.len / 5) % 32) + l
# Tagged field containing bytes
def tagged_bytes(char, l):
return tagged(char, bitstring.BitArray(l))
def trim_to_min_length(bits): def int_to_data5(val: int, *, bit_len: int = None) -> Sequence[int]:
"""Ensures 'bits' have min number of leading zeroes. """Represent big-endian number with as many 0-31 values as it takes.
Assumes 'bits' is big-endian, and that it needs to be encoded in 5 bit blocks. If `bit_len` is set, use exactly bit_len//5 values (left-padded with zeroes).
""" """
bits = bits[:] # copy if bit_len is not None:
# make sure we can be split into 5 bit blocks assert bit_len % 5 == 0, bit_len
while bits.len % 5 != 0: if val.bit_length() > bit_len:
bits.prepend('0b0') raise ValueError(f"{val=} too big for {bit_len=!r}")
# Get minimal length by trimming leading 5 bits at a time. ret = []
while bits.startswith('0b00000'): while val != 0:
if len(bits) == 5: ret.append(val % 32)
break # v == 0 val //= 32
bits = bits[5:] if bit_len is not None:
return bits ret.extend([0] * (len(ret) - bit_len // 5))
ret.reverse()
# Discard trailing bits, convert to bytes. return ret
def trim_to_bytes(barr):
# Adds a byte if necessary.
b = barr.tobytes() def int_from_data5(data5: Sequence[int]) -> int:
if barr.len % 8 != 0: total = 0
return b[:-1] for v in data5:
return b total = 32 * total + v
return total
# Try to pull out tagged data: returns tag, tagged data and remainder.
def pull_tagged(stream):
tag = stream.read(5).uint def pull_tagged(data5: bytearray) -> Tuple[str, Sequence[int]]:
length = stream.read(5).uint * 32 + stream.read(5).uint """Try to pull out tagged data: returns tag, tagged data. Mutates data in-place."""
return (CHARSET[tag], stream.read(length * 5), stream) if len(data5) < 3:
raise ValueError("Truncated field")
length = data5[1] * 32 + data5[2]
if length > len(data5) - 3:
raise ValueError(
"Truncated {} field: expected {} values".format(CHARSET[data5[0]], length))
ret = (CHARSET[data5[0]], data5[3:3+length])
del data5[:3 + length] # much faster than: data5=data5[offset:]
return ret
def lnencode(addr: 'LnAddr', privkey) -> str: def lnencode(addr: 'LnAddr', privkey) -> str:
if addr.amount: if addr.amount:
@ -179,17 +167,17 @@ def lnencode(addr: 'LnAddr', privkey) -> str:
hrp = 'ln' + amount hrp = 'ln' + amount
# Start with the timestamp # Start with the timestamp
data = bitstring.pack('uint:35', addr.date) data5 = int_to_data5(addr.date, bit_len=35)
tags_set = set() tags_set = set()
# Payment hash # Payment hash
assert addr.paymenthash is not None assert addr.paymenthash is not None
data += tagged_bytes('p', addr.paymenthash) data5 += tagged8('p', addr.paymenthash)
tags_set.add('p') tags_set.add('p')
if addr.payment_secret is not None: if addr.payment_secret is not None:
data += tagged_bytes('s', addr.payment_secret) data5 += tagged8('s', addr.payment_secret)
tags_set.add('s') tags_set.add('s')
for k, v in addr.tags: for k, v in addr.tags:
@ -202,39 +190,44 @@ def lnencode(addr: 'LnAddr', privkey) -> str:
raise LnEncodeException("Duplicate '{}' tag".format(k)) raise LnEncodeException("Duplicate '{}' tag".format(k))
if k == 'r': if k == 'r':
route = bitstring.BitArray() route = bytearray()
for step in v: for step in v:
pubkey, channel, feebase, feerate, cltv = step pubkey, scid, feebase, feerate, cltv = step
route.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)) route += pubkey
data += tagged('r', route) route += scid
route += int.to_bytes(feebase, length=4, byteorder="big", signed=False)
route += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
route += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
data5 += tagged8('r', route)
elif k == 't': elif k == 't':
pubkey, feebase, feerate, cltv = v pubkey, feebase, feerate, cltv = v
route = bitstring.BitArray(pubkey) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv) route = bytearray()
data += tagged('t', route) route += pubkey
route += int.to_bytes(feebase, length=4, byteorder="big", signed=False)
route += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
route += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
data5 += tagged8('t', route)
elif k == 'f': elif k == 'f':
if v is not None: if v is not None:
data += encode_fallback(v, addr.net) data5 += encode_fallback_addr(v, addr.net)
elif k == 'd': elif k == 'd':
# truncate to max length: 1024*5 bits = 639 bytes # truncate to max length: 1024*5 bits = 639 bytes
data += tagged_bytes('d', v.encode()[0:639]) data5 += tagged8('d', v.encode()[0:639])
elif k == 'x': elif k == 'x':
expirybits = bitstring.pack('intbe:64', v) expirybits = int_to_data5(v)
expirybits = trim_to_min_length(expirybits) data5 += tagged5('x', expirybits)
data += tagged('x', expirybits)
elif k == 'h': elif k == 'h':
data += tagged_bytes('h', sha256(v.encode('utf-8')).digest()) data5 += tagged8('h', sha256(v.encode('utf-8')).digest())
elif k == 'n': elif k == 'n':
data += tagged_bytes('n', v) data5 += tagged8('n', v)
elif k == 'c': elif k == 'c':
finalcltvbits = bitstring.pack('intbe:64', v) finalcltvbits = int_to_data5(v)
finalcltvbits = trim_to_min_length(finalcltvbits) data5 += tagged5('c', finalcltvbits)
data += tagged('c', finalcltvbits)
elif k == '9': elif k == '9':
if v == 0: if v == 0:
continue continue
feature_bits = bitstring.BitArray(uint=v, length=v.bit_length()) feature_bits = int_to_data5(v)
feature_bits = trim_to_min_length(feature_bits) data5 += tagged5('9', feature_bits)
data += tagged('9', feature_bits)
else: else:
# FIXME: Support unknown tags? # FIXME: Support unknown tags?
raise LnEncodeException("Unknown tag {}".format(k)) raise LnEncodeException("Unknown tag {}".format(k))
@ -251,15 +244,16 @@ def lnencode(addr: 'LnAddr', privkey) -> str:
raise ValueError("Must include either 'd' or 'h'") raise ValueError("Must include either 'd' or 'h'")
# We actually sign the hrp, then data (padded to 8 bits with zeroes). # We actually sign the hrp, then data (padded to 8 bits with zeroes).
msg = hrp.encode("ascii") + data.tobytes() msg = hrp.encode("ascii") + bytes(convertbits(data5, 5, 8))
msg32 = sha256(msg).digest() msg32 = sha256(msg).digest()
privkey = ecc.ECPrivkey(privkey) privkey = ecc.ECPrivkey(privkey)
sig = privkey.ecdsa_sign_recoverable(msg32, is_compressed=False) sig = privkey.ecdsa_sign_recoverable(msg32, is_compressed=False)
recovery_flag = bytes([sig[0] - 27]) recovery_flag = bytes([sig[0] - 27])
sig = bytes(sig[1:]) + recovery_flag sig = bytes(sig[1:]) + recovery_flag
data += sig sig = bytes(convertbits(sig, 8, 5, False))
data5 += sig
return bech32_encode(segwit_addr.Encoding.BECH32, hrp, bitarray_to_u5(data)) return bech32_encode(segwit_addr.Encoding.BECH32, hrp, data5)
class LnAddr(object): class LnAddr(object):
@ -393,6 +387,7 @@ class SerializableKey:
def serialize(self): def serialize(self):
return self.pubkey.get_public_key_bytes(True) return self.pubkey.get_public_key_bytes(True)
def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr: def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:
"""Parses a string into an LnAddr object. """Parses a string into an LnAddr object.
Can raise LnDecodeException or IncompatibleOrInsaneFeatures. Can raise LnDecodeException or IncompatibleOrInsaneFeatures.
@ -401,7 +396,7 @@ def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:
net = constants.net net = constants.net
decoded_bech32 = bech32_decode(invoice, ignore_long_length=True) decoded_bech32 = bech32_decode(invoice, ignore_long_length=True)
hrp = decoded_bech32.hrp hrp = decoded_bech32.hrp
data = decoded_bech32.data data5 = decoded_bech32.data # "5" as in list of 5-bit integers
if decoded_bech32.encoding is None: if decoded_bech32.encoding is None:
raise LnDecodeException("Bad bech32 checksum") raise LnDecodeException("Bad bech32 checksum")
if decoded_bech32.encoding != segwit_addr.Encoding.BECH32: if decoded_bech32.encoding != segwit_addr.Encoding.BECH32:
@ -416,13 +411,12 @@ def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:
if not hrp[2:].startswith(net.BOLT11_HRP): if not hrp[2:].startswith(net.BOLT11_HRP):
raise LnDecodeException(f"Wrong Lightning invoice HRP {hrp[2:]}, should be {net.BOLT11_HRP}") raise LnDecodeException(f"Wrong Lightning invoice HRP {hrp[2:]}, should be {net.BOLT11_HRP}")
data = u5_to_bitarray(data)
# Final signature 65 bytes, split it off. # Final signature 65 bytes, split it off.
if len(data) < 65*8: if len(data5) < 65*8//5:
raise LnDecodeException("Too short to contain signature") raise LnDecodeException("Too short to contain signature")
sigdecoded = data[-65*8:].tobytes() sigdecoded = bytes(convertbits(data5[-65*8//5:], 5, 8, False))
data = bitstring.ConstBitStream(data[:-65*8]) data5 = data5[:-65*8//5]
data5_remaining = bytearray(data5) # note: bytearray is faster than list of ints
addr = LnAddr() addr = LnAddr()
addr.pubkey = None addr.pubkey = None
@ -439,17 +433,18 @@ def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:
if amountstr != '': if amountstr != '':
addr.amount = unshorten_amount(amountstr) addr.amount = unshorten_amount(amountstr)
addr.date = data.read(35).uint addr.date = int_from_data5(data5_remaining[:7])
data5_remaining = data5_remaining[7:]
while data.pos != data.len: while data5_remaining:
tag, tagdata, data = pull_tagged(data) tag, tagdata = pull_tagged(data5_remaining) # mutates arg
# BOLT #11: # BOLT #11:
# #
# A reader MUST skip over unknown fields, an `f` field with unknown # A reader MUST skip over unknown fields, an `f` field with unknown
# `version`, or a `p`, `h`, or `n` field which does not have # `version`, or a `p`, `h`, or `n` field which does not have
# `data_length` 52, 52, or 53 respectively. # `data_length` 52, 52, or 53 respectively.
data_length = len(tagdata) / 5 data_length = len(tagdata)
if tag == 'r': if tag == 'r':
# BOLT #11: # BOLT #11:
@ -462,24 +457,43 @@ def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:
# * `feebase` (32 bits, big-endian) # * `feebase` (32 bits, big-endian)
# * `feerate` (32 bits, big-endian) # * `feerate` (32 bits, big-endian)
# * `cltv_expiry_delta` (16 bits, big-endian) # * `cltv_expiry_delta` (16 bits, big-endian)
route=[] tagdata = convertbits(tagdata, 5, 8, False)
s = bitstring.ConstBitStream(tagdata) if not tagdata:
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len: continue
route.append((s.read(264).tobytes(), route = []
s.read(64).tobytes(), with io.BytesIO(bytes(tagdata)) as s:
s.read(32).uintbe, while True:
s.read(32).uintbe, pubkey = s.read(33)
s.read(16).uintbe)) scid = s.read(8)
feebase = s.read(4)
feerate = s.read(4)
cltv = s.read(2)
if len(cltv) != 2:
break # EOF
feebase = int.from_bytes(feebase, byteorder="big")
feerate = int.from_bytes(feerate, byteorder="big")
cltv = int.from_bytes(cltv, byteorder="big")
route.append((pubkey, scid, feebase, feerate, cltv))
if route:
addr.tags.append(('r',route)) addr.tags.append(('r',route))
elif tag == 't': elif tag == 't':
s = bitstring.ConstBitStream(tagdata) tagdata = convertbits(tagdata, 5, 8, False)
e = (s.read(264).tobytes(), if not tagdata:
s.read(32).uintbe, continue
s.read(32).uintbe, route = []
s.read(16).uintbe) with io.BytesIO(bytes(tagdata)) as s:
addr.tags.append(('t', e)) pubkey = s.read(33)
feebase = s.read(4)
feerate = s.read(4)
cltv = s.read(2)
if len(cltv) == 2: # no EOF
feebase = int.from_bytes(feebase, byteorder="big")
feerate = int.from_bytes(feerate, byteorder="big")
cltv = int.from_bytes(cltv, byteorder="big")
route.append((pubkey, feebase, feerate, cltv))
addr.tags.append(('t', route))
elif tag == 'f': elif tag == 'f':
fallback = parse_fallback(tagdata, addr.net) fallback = parse_fallback_addr(tagdata, addr.net)
if fallback: if fallback:
addr.tags.append(('f', fallback)) addr.tags.append(('f', fallback))
else: else:
@ -488,41 +502,41 @@ def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:
continue continue
elif tag == 'd': elif tag == 'd':
addr.tags.append(('d', trim_to_bytes(tagdata).decode('utf-8'))) addr.tags.append(('d', bytes(convertbits(tagdata, 5, 8, False)).decode('utf-8')))
elif tag == 'h': elif tag == 'h':
if data_length != 52: if data_length != 52:
addr.unknown_tags.append((tag, tagdata)) addr.unknown_tags.append((tag, tagdata))
continue continue
addr.tags.append(('h', trim_to_bytes(tagdata))) addr.tags.append(('h', bytes(convertbits(tagdata, 5, 8, False))))
elif tag == 'x': elif tag == 'x':
addr.tags.append(('x', tagdata.uint)) addr.tags.append(('x', int_from_data5(tagdata)))
elif tag == 'p': elif tag == 'p':
if data_length != 52: if data_length != 52:
addr.unknown_tags.append((tag, tagdata)) addr.unknown_tags.append((tag, tagdata))
continue continue
addr.paymenthash = trim_to_bytes(tagdata) addr.paymenthash = bytes(convertbits(tagdata, 5, 8, False))
elif tag == 's': elif tag == 's':
if data_length != 52: if data_length != 52:
addr.unknown_tags.append((tag, tagdata)) addr.unknown_tags.append((tag, tagdata))
continue continue
addr.payment_secret = trim_to_bytes(tagdata) addr.payment_secret = bytes(convertbits(tagdata, 5, 8, False))
elif tag == 'n': elif tag == 'n':
if data_length != 53: if data_length != 53:
addr.unknown_tags.append((tag, tagdata)) addr.unknown_tags.append((tag, tagdata))
continue continue
pubkeybytes = trim_to_bytes(tagdata) pubkeybytes = bytes(convertbits(tagdata, 5, 8, False))
addr.pubkey = pubkeybytes addr.pubkey = pubkeybytes
elif tag == 'c': elif tag == 'c':
addr.tags.append(('c', tagdata.uint)) addr.tags.append(('c', int_from_data5(tagdata)))
elif tag == '9': elif tag == '9':
features = tagdata.uint features = int_from_data5(tagdata)
addr.tags.append(('9', features)) addr.tags.append(('9', features))
# note: The features are not validated here in the parser, # note: The features are not validated here in the parser,
# instead, validation is done just before we try paying the invoice (in lnworker._check_invoice). # instead, validation is done just before we try paying the invoice (in lnworker._check_invoice).
@ -536,16 +550,17 @@ def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr:
print('hex of signature data (32 byte r, 32 byte s): {}' print('hex of signature data (32 byte r, 32 byte s): {}'
.format(hexlify(sigdecoded[0:64]))) .format(hexlify(sigdecoded[0:64])))
print('recovery flag: {}'.format(sigdecoded[64])) print('recovery flag: {}'.format(sigdecoded[64]))
data8 = bytes(convertbits(data5, 5, 8, True))
print('hex of data for signing: {}' print('hex of data for signing: {}'
.format(hexlify(hrp.encode("ascii") + data.tobytes()))) .format(hexlify(hrp.encode("ascii") + data8)))
print('SHA256 of above: {}'.format(sha256(hrp.encode("ascii") + data.tobytes()).hexdigest())) print('SHA256 of above: {}'.format(sha256(hrp.encode("ascii") + data8).hexdigest()))
# BOLT #11: # BOLT #11:
# #
# A reader MUST check that the `signature` is valid (see the `n` tagged # A reader MUST check that the `signature` is valid (see the `n` tagged
# field specified below). # field specified below).
addr.signature = sigdecoded[:65] addr.signature = sigdecoded[:65]
hrp_hash = sha256(hrp.encode("ascii") + data.tobytes()).digest() hrp_hash = sha256(hrp.encode("ascii") + bytes(convertbits(data5, 5, 8, True))).digest()
if addr.pubkey: # Specified by `n` if addr.pubkey: # Specified by `n`
# BOLT #11: # BOLT #11:
# #

8
electrum/segwit_addr.py

@ -22,10 +22,10 @@
"""Reference implementation for Bech32/Bech32m and segwit addresses.""" """Reference implementation for Bech32/Bech32m and segwit addresses."""
from enum import Enum from enum import Enum
from typing import Tuple, Optional, Sequence, NamedTuple, List from typing import Tuple, Optional, Sequence, NamedTuple, List, Mapping, Iterable
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
_CHARSET_INVERSE = {c: i for (i, c) in enumerate(CHARSET)} CHARSET_INVERSE = {c: i for (i, c) in enumerate(CHARSET)} # type: Mapping[str, int]
BECH32_CONST = 1 BECH32_CONST = 1
BECH32M_CONST = 0x2bc830a3 BECH32M_CONST = 0x2bc830a3
@ -99,7 +99,7 @@ def bech32_decode(bech: str, *, ignore_long_length=False) -> DecodedBech32:
bech = bech_lower bech = bech_lower
hrp = bech[:pos] hrp = bech[:pos]
try: try:
data = [_CHARSET_INVERSE[x] for x in bech[pos+1:]] data = [CHARSET_INVERSE[x] for x in bech[pos + 1:]]
except KeyError: except KeyError:
return DecodedBech32(None, None, None) return DecodedBech32(None, None, None)
encoding = bech32_verify_checksum(hrp, data) encoding = bech32_verify_checksum(hrp, data)
@ -108,7 +108,7 @@ def bech32_decode(bech: str, *, ignore_long_length=False) -> DecodedBech32:
return DecodedBech32(encoding=encoding, hrp=hrp, data=data[:-6]) return DecodedBech32(encoding=encoding, hrp=hrp, data=data[:-6])
def convertbits(data, frombits, tobits, pad=True): def convertbits(data: Iterable[int], frombits: int, tobits: int, pad: bool = True) -> Optional[Sequence[int]]:
"""General power-of-2 base conversion.""" """General power-of-2 base conversion."""
acc = 0 acc = 0
bits = 0 bits = 0

52
electrum/trampoline.py

@ -1,8 +1,7 @@
import io
import os import os
import bitstring
import random import random
from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List, Iterable, Sequence, Set, Any
from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List, Iterable, Sequence, Set
from .lnutil import LnFeatures, PaymentFeeBudget from .lnutil import LnFeatures, PaymentFeeBudget
from .lnonion import calc_hops_data_for_payment, new_onion_packet, OnionPacket from .lnonion import calc_hops_data_for_payment, new_onion_packet, OnionPacket
@ -91,32 +90,37 @@ def trampolines_by_id():
def is_hardcoded_trampoline(node_id: bytes) -> bool: def is_hardcoded_trampoline(node_id: bytes) -> bool:
return node_id in trampolines_by_id() return node_id in trampolines_by_id()
def encode_routing_info(r_tags): def encode_routing_info(r_tags: Sequence[Sequence[Sequence[Any]]]) -> bytes:
result = bitstring.BitArray() result = bytearray()
for route in r_tags: for route in r_tags:
result.append(bitstring.pack('uint:8', len(route))) result += bytes([len(route)])
for step in route: for step in route:
pubkey, scid, feebase, feerate, cltv = step pubkey, scid, feebase, feerate, cltv = step
result.append( result += pubkey
bitstring.BitArray(pubkey) \ result += scid
+ bitstring.BitArray(scid)\ result += int.to_bytes(feebase, length=4, byteorder="big", signed=False)
+ bitstring.pack('intbe:32', feebase)\ result += int.to_bytes(feerate, length=4, byteorder="big", signed=False)
+ bitstring.pack('intbe:32', feerate)\ result += int.to_bytes(cltv, length=2, byteorder="big", signed=False)
+ bitstring.pack('intbe:16', cltv)) return bytes(result)
return result.tobytes()
def decode_routing_info(s: bytes): def decode_routing_info(rinfo: bytes) -> Sequence[Sequence[Sequence[Any]]]:
s = bitstring.BitArray(s) if not rinfo:
return []
r_tags = [] r_tags = []
n = 8*(33 + 8 + 4 + 4 + 2) with io.BytesIO(bytes(rinfo)) as s:
while s: while True:
route = [] route = []
length, s = s[0:8], s[8:] route_len = s.read(1)
length = length.unpack('uint:8')[0] if not route_len:
for i in range(length): break
chunk, s = s[0:n], s[n:] for step in range(route_len[0]):
item = chunk.unpack('bytes:33, bytes:8, intbe:32, intbe:32, intbe:16') pubkey = s.read(33)
route.append(item) scid = s.read(8)
feebase = int.from_bytes(s.read(4), byteorder="big")
feerate = int.from_bytes(s.read(4), byteorder="big")
cltv = int.from_bytes(s.read(2), byteorder="big")
route.append((pubkey, scid, feebase, feerate, cltv))
r_tags.append(route) r_tags.append(route)
return r_tags return r_tags

16
tests/test_bolt11.py

@ -4,7 +4,7 @@ from binascii import unhexlify, hexlify
import pprint import pprint
import unittest import unittest
from electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, lndecode, u5_to_bitarray, bitarray_to_u5 from electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, lndecode
from electrum.segwit_addr import bech32_encode, bech32_decode from electrum.segwit_addr import bech32_encode, bech32_decode
from electrum import segwit_addr from electrum import segwit_addr
from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage, LnFeatures, IncompatibleLightningFeatures from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage, LnFeatures, IncompatibleLightningFeatures
@ -125,19 +125,17 @@ class TestBolt11(ElectrumTestCase):
_, hrp, data = bech32_decode( _, hrp, data = bech32_decode(
lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('9', 33282)]), PRIVKEY), lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('9', 33282)]), PRIVKEY),
ignore_long_length=True) ignore_long_length=True)
databits = u5_to_bitarray(data) data[-1] ^= 1
databits.invert(-1) lnaddr = lndecode(bech32_encode(segwit_addr.Encoding.BECH32, hrp, data), verbose=True)
lnaddr = lndecode(bech32_encode(segwit_addr.Encoding.BECH32, hrp, bitarray_to_u5(databits)), verbose=True) self.assertNotEqual(lnaddr.pubkey.serialize(), PUBKEY)
assert lnaddr.pubkey.serialize() != PUBKEY
# But not if we supply expliciy `n` specifier! # But not if we supply expliciy `n` specifier!
_, hrp, data = bech32_decode( _, hrp, data = bech32_decode(
lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('n', PUBKEY), ('9', 33282)]), PRIVKEY), lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('n', PUBKEY), ('9', 33282)]), PRIVKEY),
ignore_long_length=True) ignore_long_length=True)
databits = u5_to_bitarray(data) data[-1] ^= 1
databits.invert(-1) lnaddr = lndecode(bech32_encode(segwit_addr.Encoding.BECH32, hrp, data), verbose=True)
lnaddr = lndecode(bech32_encode(segwit_addr.Encoding.BECH32, hrp, bitarray_to_u5(databits)), verbose=True) self.assertEqual(lnaddr.pubkey.serialize(), PUBKEY)
assert lnaddr.pubkey.serialize() == PUBKEY
def test_min_final_cltv_expiry_decoding(self): def test_min_final_cltv_expiry_decoding(self):
lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qsp5qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsdqqcqzys9qypqsqp2h6a5xeytuc3fad2ed4gxvhd593lwjdna3dxsyeem0qkzjx6guk44jend0xq4zzvp6f3fy07wnmxezazzsxgmvqee8shxjuqu2eu0qpnvc95x", lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qsp5qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsdqqcqzys9qypqsqp2h6a5xeytuc3fad2ed4gxvhd593lwjdna3dxsyeem0qkzjx6guk44jend0xq4zzvp6f3fy07wnmxezazzsxgmvqee8shxjuqu2eu0qpnvc95x",

Loading…
Cancel
Save