Merge pull request #9020 from SomberNight/202404_bitstring2
dependencies: remove bitstring
This commit is contained in:
@@ -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 \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 encode_fallback_addr(fallback: str, net: Type[AbstractNet]) -> Sequence[int]:
|
||||||
def u5_to_bitarray(arr):
|
"""Encode all supported fallback addresses."""
|
||||||
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]):
|
|
||||||
""" 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",
|
|
||||||
CHARSET.find(char),
|
|
||||||
(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 tagged8(char: str, data8: Sequence[int]) -> Sequence[int]:
|
||||||
"""Ensures 'bits' have min number of leading zeroes.
|
return tagged5(char, convertbits(data8, 8, 5))
|
||||||
Assumes 'bits' is big-endian, and that it needs to be encoded in 5 bit blocks.
|
|
||||||
|
|
||||||
|
def int_to_data5(val: int, *, bit_len: int = None) -> Sequence[int]:
|
||||||
|
"""Represent big-endian number with as many 0-31 values as it takes.
|
||||||
|
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()
|
||||||
|
return ret
|
||||||
|
|
||||||
# Discard trailing bits, convert to bytes.
|
|
||||||
def trim_to_bytes(barr):
|
|
||||||
# Adds a byte if necessary.
|
|
||||||
b = barr.tobytes()
|
|
||||||
if barr.len % 8 != 0:
|
|
||||||
return b[:-1]
|
|
||||||
return b
|
|
||||||
|
|
||||||
# Try to pull out tagged data: returns tag, tagged data and remainder.
|
def int_from_data5(data5: Sequence[int]) -> int:
|
||||||
def pull_tagged(stream):
|
total = 0
|
||||||
tag = stream.read(5).uint
|
for v in data5:
|
||||||
length = stream.read(5).uint * 32 + stream.read(5).uint
|
total = 32 * total + v
|
||||||
return (CHARSET[tag], stream.read(length * 5), stream)
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def pull_tagged(data5: bytearray) -> Tuple[str, Sequence[int]]:
|
||||||
|
"""Try to pull out tagged data: returns tag, tagged data. Mutates data in-place."""
|
||||||
|
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)
|
||||||
addr.tags.append(('r',route))
|
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))
|
||||||
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:
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,33 +90,38 @@ 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):
|
|
||||||
s = bitstring.BitArray(s)
|
def decode_routing_info(rinfo: bytes) -> Sequence[Sequence[Sequence[Any]]]:
|
||||||
|
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)
|
||||||
r_tags.append(route)
|
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)
|
||||||
return r_tags
|
return r_tags
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user