4 changed files with 524 additions and 2 deletions
@ -0,0 +1 @@
|
||||
This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23 |
||||
@ -0,0 +1,123 @@
|
||||
# Copyright (c) 2017 Pieter Wuille |
||||
# |
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
# of this software and associated documentation files (the "Software"), to deal |
||||
# in the Software without restriction, including without limitation the rights |
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
# copies of the Software, and to permit persons to whom the Software is |
||||
# furnished to do so, subject to the following conditions: |
||||
# |
||||
# The above copyright notice and this permission notice shall be included in |
||||
# all copies or substantial portions of the Software. |
||||
# |
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
# THE SOFTWARE. |
||||
|
||||
"""Reference implementation for Bech32 and segwit addresses.""" |
||||
|
||||
|
||||
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" |
||||
|
||||
|
||||
def bech32_polymod(values): |
||||
"""Internal function that computes the Bech32 checksum.""" |
||||
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] |
||||
chk = 1 |
||||
for value in values: |
||||
top = chk >> 25 |
||||
chk = (chk & 0x1ffffff) << 5 ^ value |
||||
for i in range(5): |
||||
chk ^= generator[i] if ((top >> i) & 1) else 0 |
||||
return chk |
||||
|
||||
|
||||
def bech32_hrp_expand(hrp): |
||||
"""Expand the HRP into values for checksum computation.""" |
||||
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] |
||||
|
||||
|
||||
def bech32_verify_checksum(hrp, data): |
||||
"""Verify a checksum given HRP and converted data characters.""" |
||||
return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 |
||||
|
||||
|
||||
def bech32_create_checksum(hrp, data): |
||||
"""Compute the checksum values given HRP and data.""" |
||||
values = bech32_hrp_expand(hrp) + data |
||||
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 |
||||
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] |
||||
|
||||
|
||||
def bech32_encode(hrp, data): |
||||
"""Compute a Bech32 string given HRP and data values.""" |
||||
combined = data + bech32_create_checksum(hrp, data) |
||||
return hrp + '1' + ''.join([CHARSET[d] for d in combined]) |
||||
|
||||
|
||||
def bech32_decode(bech): |
||||
"""Validate a Bech32 string, and determine HRP and data.""" |
||||
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or |
||||
(bech.lower() != bech and bech.upper() != bech)): |
||||
return (None, None) |
||||
bech = bech.lower() |
||||
pos = bech.rfind('1') |
||||
if pos < 1 or pos + 7 > len(bech): #or len(bech) > 90: |
||||
return (None, None) |
||||
if not all(x in CHARSET for x in bech[pos+1:]): |
||||
return (None, None) |
||||
hrp = bech[:pos] |
||||
data = [CHARSET.find(x) for x in bech[pos+1:]] |
||||
if not bech32_verify_checksum(hrp, data): |
||||
return (None, None) |
||||
return (hrp, data[:-6]) |
||||
|
||||
|
||||
def convertbits(data, frombits, tobits, pad=True): |
||||
"""General power-of-2 base conversion.""" |
||||
acc = 0 |
||||
bits = 0 |
||||
ret = [] |
||||
maxv = (1 << tobits) - 1 |
||||
max_acc = (1 << (frombits + tobits - 1)) - 1 |
||||
for value in data: |
||||
if value < 0 or (value >> frombits): |
||||
return None |
||||
acc = ((acc << frombits) | value) & max_acc |
||||
bits += frombits |
||||
while bits >= tobits: |
||||
bits -= tobits |
||||
ret.append((acc >> bits) & maxv) |
||||
if pad: |
||||
if bits: |
||||
ret.append((acc << (tobits - bits)) & maxv) |
||||
elif bits >= frombits or ((acc << (tobits - bits)) & maxv): |
||||
return None |
||||
return ret |
||||
|
||||
|
||||
def decode(hrp, addr): |
||||
"""Decode a segwit address.""" |
||||
hrpgot, data = bech32_decode(addr) |
||||
if hrpgot != hrp: |
||||
return (None, None) |
||||
decoded = convertbits(data[1:], 5, 8, False) |
||||
if decoded is None or len(decoded) < 2 or len(decoded) > 40: |
||||
return (None, None) |
||||
if data[0] > 16: |
||||
return (None, None) |
||||
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: |
||||
return (None, None) |
||||
return (data[0], decoded) |
||||
|
||||
|
||||
def encode(hrp, witver, witprog): |
||||
"""Encode a segwit address.""" |
||||
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) |
||||
assert decode(hrp, ret) is not (None, None) |
||||
return ret |
||||
|
||||
@ -0,0 +1,385 @@
|
||||
#! /usr/bin/env python3 |
||||
import traceback |
||||
import ecdsa.curves |
||||
from ..bitcoin import MyVerifyingKey, GetPubKey |
||||
from .bech32 import bech32_encode, bech32_decode, CHARSET |
||||
from binascii import hexlify, unhexlify |
||||
from bitstring import BitArray |
||||
from decimal import Decimal |
||||
|
||||
import bitstring |
||||
import hashlib |
||||
import math |
||||
import re |
||||
import sys |
||||
import time |
||||
|
||||
|
||||
# BOLT #11: |
||||
# |
||||
# A writer MUST encode `amount` as a positive decimal integer with no |
||||
# leading zeroes, SHOULD use the shortest representation possible. |
||||
def shorten_amount(amount): |
||||
""" Given an amount in bitcoin, shorten it |
||||
""" |
||||
# Convert to pico initially |
||||
amount = int(amount * 10**12) |
||||
units = ['p', 'n', 'u', 'm', ''] |
||||
for unit in units: |
||||
if amount % 1000 == 0: |
||||
amount //= 1000 |
||||
else: |
||||
break |
||||
return str(amount) + unit |
||||
|
||||
def unshorten_amount(amount): |
||||
""" Given a shortened amount, convert it into a decimal |
||||
""" |
||||
# BOLT #11: |
||||
# The following `multiplier` letters are defined: |
||||
# |
||||
#* `m` (milli): multiply by 0.001 |
||||
#* `u` (micro): multiply by 0.000001 |
||||
#* `n` (nano): multiply by 0.000000001 |
||||
#* `p` (pico): multiply by 0.000000000001 |
||||
units = { |
||||
'p': 10**12, |
||||
'n': 10**9, |
||||
'u': 10**6, |
||||
'm': 10**3, |
||||
} |
||||
unit = str(amount)[-1] |
||||
# BOLT #11: |
||||
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by |
||||
# anything except a `multiplier` in the table above. |
||||
if not re.fullmatch("\d+[pnum]?", str(amount)): |
||||
raise ValueError("Invalid amount '{}'".format(amount)) |
||||
|
||||
if unit in units.keys(): |
||||
return Decimal(amount[:-1]) / units[unit] |
||||
else: |
||||
return Decimal(amount) |
||||
|
||||
# Bech32 spits out array of 5-bit values. Shim here. |
||||
def u5_to_bitarray(arr): |
||||
ret = bitstring.BitArray() |
||||
for a in arr: |
||||
ret += bitstring.pack("uint:5", a) |
||||
return ret |
||||
|
||||
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, currency): |
||||
""" Encode all supported fallback addresses. |
||||
""" |
||||
if currency == 'bc' or currency == 'tb': |
||||
fbhrp, witness = bech32_decode(fallback) |
||||
if fbhrp: |
||||
if fbhrp != currency: |
||||
raise ValueError("Not a bech32 address for this currency") |
||||
wver = witness[0] |
||||
if wver > 16: |
||||
raise ValueError("Invalid witness version {}".format(witness[0])) |
||||
wprog = u5_to_bitarray(witness[1:]) |
||||
else: |
||||
addr = base58.b58decode_check(fallback) |
||||
if is_p2pkh(currency, addr[0]): |
||||
wver = 17 |
||||
elif is_p2sh(currency, addr[0]): |
||||
wver = 18 |
||||
else: |
||||
raise ValueError("Unknown address type for {}".format(currency)) |
||||
wprog = addr[1:] |
||||
return tagged('f', bitstring.pack("uint:5", wver) + wprog) |
||||
else: |
||||
raise NotImplementedError("Support for currency {} not implemented".format(currency)) |
||||
|
||||
def parse_fallback(fallback, currency): |
||||
return None # this function disabled by Janus to avoid base58 dependency |
||||
if currency == 'bc' or currency == 'tb': |
||||
wver = fallback[0:5].uint |
||||
if wver == 17: |
||||
addr=base58.b58encode_check(bytes([base58_prefix_map[currency][0]]) |
||||
+ fallback[5:].tobytes()) |
||||
elif wver == 18: |
||||
addr=base58.b58encode_check(bytes([base58_prefix_map[currency][1]]) |
||||
+ fallback[5:].tobytes()) |
||||
elif wver <= 16: |
||||
addr=bech32_encode(currency, bitarray_to_u5(fallback)) |
||||
else: |
||||
return None |
||||
else: |
||||
addr=fallback.tobytes() |
||||
return addr |
||||
|
||||
|
||||
# Map of classical and witness address prefixes |
||||
base58_prefix_map = { |
||||
'bc' : (0, 5), |
||||
'tb' : (111, 196) |
||||
} |
||||
|
||||
def is_p2pkh(currency, prefix): |
||||
return prefix == base58_prefix_map[currency][0] |
||||
|
||||
def is_p2sh(currency, prefix): |
||||
return prefix == base58_prefix_map[currency][1] |
||||
|
||||
# Tagged field containing BitArray |
||||
def tagged(char, l): |
||||
# Tagged fields need to be zero-padded to 5 bits. |
||||
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)) |
||||
|
||||
# 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 pull_tagged(stream): |
||||
tag = stream.read(5).uint |
||||
length = stream.read(5).uint * 32 + stream.read(5).uint |
||||
return (CHARSET[tag], stream.read(length * 5), stream) |
||||
|
||||
def lnencode(addr, privkey): |
||||
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) |
||||
else: |
||||
amount = addr.currency if addr.currency else '' |
||||
|
||||
hrp = 'ln' + amount |
||||
|
||||
# Start with the timestamp |
||||
data = bitstring.pack('uint:35', addr.date) |
||||
|
||||
# Payment hash |
||||
data += tagged_bytes('p', addr.paymenthash) |
||||
tags_set = set() |
||||
|
||||
for k, v in addr.tags: |
||||
|
||||
# BOLT #11: |
||||
# |
||||
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields, |
||||
if k in ('d', 'h', 'n', 'x'): |
||||
if k in tags_set: |
||||
raise ValueError("Duplicate '{}' tag".format(k)) |
||||
|
||||
if k == 'r': |
||||
route = bitstring.BitArray() |
||||
for step in v: |
||||
pubkey, channel, 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)) |
||||
data += tagged('r', route) |
||||
elif k == 'f': |
||||
data += encode_fallback(v, addr.currency) |
||||
elif k == 'd': |
||||
data += tagged_bytes('d', v.encode()) |
||||
elif k == 'x': |
||||
# Get minimal length by trimming leading 5 bits at a time. |
||||
expirybits = bitstring.pack('intbe:64', v)[4:64] |
||||
while expirybits.startswith('0b00000'): |
||||
expirybits = expirybits[5:] |
||||
data += tagged('x', expirybits) |
||||
elif k == 'h': |
||||
data += tagged_bytes('h', hashlib.sha256(v.encode('utf-8')).digest()) |
||||
elif k == 'n': |
||||
data += tagged_bytes('n', v) |
||||
else: |
||||
# FIXME: Support unknown tags? |
||||
raise ValueError("Unknown tag {}".format(k)) |
||||
|
||||
tags_set.add(k) |
||||
|
||||
# BOLT #11: |
||||
# |
||||
# A writer MUST include either a `d` or `h` field, and MUST NOT include |
||||
# both. |
||||
if 'd' in tags_set and 'h' in tags_set: |
||||
raise ValueError("Cannot include both 'd' and 'h'") |
||||
if not 'd' in tags_set and not 'h' in tags_set: |
||||
raise ValueError("Must include either 'd' or 'h'") |
||||
|
||||
# We actually sign the hrp, then data (padded to 8 bits with zeroes). |
||||
privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey))) |
||||
sig = privkey.ecdsa_sign_recoverable(bytearray([ord(c) for c in hrp]) + data.tobytes()) |
||||
# This doesn't actually serialize, but returns a pair of values :( |
||||
sig, recid = privkey.ecdsa_recoverable_serialize(sig) |
||||
data += bytes(sig) + bytes([recid]) |
||||
|
||||
return bech32_encode(hrp, bitarray_to_u5(data)) |
||||
|
||||
class LnAddr(object): |
||||
def __init__(self, paymenthash=None, amount=None, currency='bc', tags=None, date=None): |
||||
self.date = int(time.time()) if not date else int(date) |
||||
self.tags = [] if not tags else tags |
||||
self.unknown_tags = [] |
||||
self.paymenthash=paymenthash |
||||
self.signature = None |
||||
self.pubkey = None |
||||
self.currency = currency |
||||
self.amount = amount |
||||
|
||||
def __str__(self): |
||||
return "LnAddr[{}, amount={}{} tags=[{}]]".format( |
||||
hexlify(self.pubkey.serialize()).decode('utf-8'), |
||||
self.amount, self.currency, |
||||
", ".join([k + '=' + str(v) for k, v in self.tags]) |
||||
) |
||||
|
||||
def lndecode(a, verbose=False): |
||||
hrp, data = bech32_decode(a) |
||||
if not hrp: |
||||
raise ValueError("Bad bech32 checksum") |
||||
|
||||
# BOLT #11: |
||||
# |
||||
# A reader MUST fail if it does not understand the `prefix`. |
||||
if not hrp.startswith('ln'): |
||||
raise ValueError("Does not start with ln") |
||||
|
||||
data = u5_to_bitarray(data); |
||||
|
||||
# Final signature 65 bytes, split it off. |
||||
if len(data) < 65*8: |
||||
raise ValueError("Too short to contain signature") |
||||
sigdecoded = data[-65*8:].tobytes() |
||||
data = bitstring.ConstBitStream(data[:-65*8]) |
||||
|
||||
addr = LnAddr() |
||||
addr.pubkey = None |
||||
|
||||
m = re.search("[^\d]+", hrp[2:]) |
||||
if m: |
||||
addr.currency = m.group(0) |
||||
amountstr = hrp[2+m.end():] |
||||
# BOLT #11: |
||||
# |
||||
# A reader SHOULD indicate if amount is unspecified, otherwise it MUST |
||||
# multiply `amount` by the `multiplier` value (if any) to derive the |
||||
# amount required for payment. |
||||
if amountstr != '': |
||||
addr.amount = unshorten_amount(amountstr) |
||||
|
||||
addr.date = data.read(35).uint |
||||
|
||||
while data.pos != data.len: |
||||
tag, tagdata, data = pull_tagged(data) |
||||
|
||||
# BOLT #11: |
||||
# |
||||
# A reader MUST skip over unknown fields, an `f` field with unknown |
||||
# `version`, or a `p`, `h`, or `n` field which does not have |
||||
# `data_length` 52, 52, or 53 respectively. |
||||
data_length = len(tagdata) / 5 |
||||
|
||||
if tag == 'r': |
||||
# BOLT #11: |
||||
# |
||||
# * `r` (3): `data_length` variable. One or more entries |
||||
# containing extra routing information for a private route; |
||||
# there may be more than one `r` field, too. |
||||
# * `pubkey` (264 bits) |
||||
# * `short_channel_id` (64 bits) |
||||
# * `feebase` (32 bits, big-endian) |
||||
# * `feerate` (32 bits, big-endian) |
||||
# * `cltv_expiry_delta` (16 bits, big-endian) |
||||
route=[] |
||||
s = bitstring.ConstBitStream(tagdata) |
||||
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len: |
||||
route.append((s.read(264).tobytes(), |
||||
s.read(64).tobytes(), |
||||
s.read(32).intbe, |
||||
s.read(32).intbe, |
||||
s.read(16).intbe)) |
||||
addr.tags.append(('r',route)) |
||||
elif tag == 'f': |
||||
fallback = parse_fallback(tagdata, addr.currency) |
||||
if fallback: |
||||
addr.tags.append(('f', fallback)) |
||||
else: |
||||
# Incorrect version. |
||||
addr.unknown_tags.append((tag, tagdata)) |
||||
continue |
||||
|
||||
elif tag == 'd': |
||||
addr.tags.append(('d', trim_to_bytes(tagdata).decode('utf-8'))) |
||||
|
||||
elif tag == 'h': |
||||
if data_length != 52: |
||||
addr.unknown_tags.append((tag, tagdata)) |
||||
continue |
||||
addr.tags.append(('h', trim_to_bytes(tagdata))) |
||||
|
||||
elif tag == 'x': |
||||
addr.tags.append(('x', tagdata.uint)) |
||||
|
||||
elif tag == 'p': |
||||
if data_length != 52: |
||||
addr.unknown_tags.append((tag, tagdata)) |
||||
continue |
||||
addr.paymenthash = trim_to_bytes(tagdata) |
||||
|
||||
elif tag == 'n': |
||||
if data_length != 53: |
||||
addr.unknown_tags.append((tag, tagdata)) |
||||
continue |
||||
addr.pubkey = secp256k1.PublicKey(flags=secp256k1.ALL_FLAGS) |
||||
addr.pubkey.deserialize(trim_to_bytes(tagdata)) |
||||
else: |
||||
addr.unknown_tags.append((tag, tagdata)) |
||||
|
||||
if verbose: |
||||
print('hex of signature data (32 byte r, 32 byte s): {}' |
||||
.format(hexlify(sigdecoded[0:64]))) |
||||
print('recovery flag: {}'.format(sigdecoded[64])) |
||||
print('hex of data for signing: {}' |
||||
.format(hexlify(bytearray([ord(c) for c in hrp]) |
||||
+ data.tobytes()))) |
||||
print('SHA256 of above: {}'.format(hashlib.sha256(bytearray([ord(c) for c in hrp]) + data.tobytes()).hexdigest())) |
||||
|
||||
# BOLT #11: |
||||
# |
||||
# A reader MUST check that the `signature` is valid (see the `n` tagged |
||||
# field specified below). |
||||
if addr.pubkey: # Specified by `n` |
||||
# BOLT #11: |
||||
# |
||||
# A reader MUST use the `n` field to validate the signature instead of |
||||
# performing signature recovery if a valid `n` field is provided. |
||||
addr.signature = addr.pubkey.ecdsa_deserialize_compact(sigdecoded[0:64]) |
||||
if not addr.pubkey.ecdsa_verify(bytearray([ord(c) for c in hrp]) + data.tobytes(), addr.signature): |
||||
raise ValueError('Invalid signature') |
||||
else: # Recover pubkey from signature. |
||||
addr.pubkey = SerializableKey(MyVerifyingKey.from_signature(sigdecoded[:64], sigdecoded[64], hashlib.sha256(bytearray([ord(c) for c in hrp]) + data.tobytes()).digest(), curve = ecdsa.curves.SECP256k1)) |
||||
|
||||
return addr |
||||
|
||||
class SerializableKey: |
||||
def __init__(self, pubkey): |
||||
self.pubkey = pubkey |
||||
def serialize(self): |
||||
return GetPubKey(self.pubkey.pubkey, True) |
||||
Loading…
Reference in new issue