commit afa5797099cff6e448f07596a6c501a9e67fe208
parent 1ab03e8b2a5745b8aa999ceea14b11ff4b0b97ff
Author: Janus <ysangkok@gmail.com>
Date: Wed, 28 Mar 2018 15:41:51 +0200
lightning: kivy: open channel button in invoice
Diffstat:
4 files changed, 524 insertions(+), 2 deletions(-)
diff --git a/gui/kivy/uix/dialogs/lightning_payer.py b/gui/kivy/uix/dialogs/lightning_payer.py
@@ -1,7 +1,9 @@
+import binascii
from kivy.lang import Builder
from kivy.factory import Factory
from electrum_gui.kivy.i18n import _
import electrum.lightning as lightning
+from electrum.lightning_payencode.lnaddr import lndecode
Builder.load_string('''
<LightningPayerDialog@Popup>
@@ -34,6 +36,11 @@ Builder.load_string('''
Button:
size_hint: 1, None
height: '48dp'
+ text: _('Open channel to pubkey in invoice')
+ on_release: s.do_open_channel()
+ Button:
+ size_hint: 1, None
+ height: '48dp'
text: _('Pay pasted/scanned invoice')
on_release: s.do_pay()
''')
@@ -63,7 +70,13 @@ class LightningPayerDialog(Factory.Popup):
self.invoice_data = contents
def do_clear(self):
self.invoice_data = ""
+ def do_open_channel(self):
+ compressed_pubkey_bytes = lndecode(self.invoice_data).pubkey.serialize()
+ hexpubkey = binascii.hexlify(compressed_pubkey_bytes).decode("ascii")
+ local_amt = 100000
+ push_amt = 0
+ lightning.lightningCall(self.app.wallet.network.lightningrpc, "openchannel")(hexpubkey, local_amt, push_amt)
def do_pay(self):
lightning.lightningCall(self.app.wallet.network.lightningrpc, "sendpayment")("--pay_req=" + self.invoice_data)
- def on_lightning_qr(self):
- self.app.show_info("Lightning Invoice QR scanning not implemented") #TODO
+ def on_lightning_qr(self, data):
+ self.invoice_data = str(data)
diff --git a/lib/lightning_payencode/FORKED b/lib/lightning_payencode/FORKED
@@ -0,0 +1 @@
+This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23
diff --git a/lib/lightning_payencode/bech32.py b/lib/lightning_payencode/bech32.py
@@ -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
+
diff --git a/lib/lightning_payencode/lnaddr.py b/lib/lightning_payencode/lnaddr.py
@@ -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)