Skip to content

Instantly share code, notes, and snippets.

@fiatjaf fiatjaf/.gitignore
Last active May 16, 2020

What would you like to do?
hsm_secret and custom invoices with lnurl on c-lightning

How to use this gist

You have two options:

  1. Read all the comments and source code
  2. Run stuff without knowing what it does

This will print your node's private key. Take it and pass it to the next command:

python <privatekey>

This will print data for the custom invoice.

Please let me know if you didn't understand something or if anything is broken.

This gist uses slightly modified code from Hopefully Rusty won't sue me.

# extracting the node's private key from the hsm_secret file
# first thing: read the $LIGHTNING_DIR/hsm_secret
# xxd -p ~/.lightning/hsm_secret | tr -d '\n' && echo ""
import sys
hex_value = "" or sys.argv[-1]
# if you have it hex-encoded (as given from the xxd line above)
if hex_value and len(hex_value) > 60:
# proceed to make it a binary string again
from binascii import unhexlify
hsm_secret = unhexlify(hex_value)
# or read it directly
from os.path import expanduser
hsm_secret = open(expanduser("~/.lightning/hsm_secret"), "rb").read()
# to generate the node private key, you must apply this hkdf thing
# (which is a special way to an hmac) to id
import hashlib
from hkdf import Hkdf
salt = bytes([0]) or b"\x00"
key = Hkdf(salt, hsm_secret, hash=hashlib.sha256).expand(b"nodeid")
# we're done here.
# however, in the c-lightning code they say there's a ridiculously small chance of the
# key produced here not being valid to the secp256k1 parameters, so they test it
# and increase the salt until it is valid.
# if for some reason your key is not correct with salt 0 you can just increase it by 1
# and be fine (in the majority of cases you can just use salt 0 and be fine)
# how to test? I don't know, maybe secp256k1 will raise an exception if the key is wrong?
import secp256k1
i = 0
while True:
salt = bytes([i])
key = Hkdf(salt, hsm_secret, hash=hashlib.sha256).expand(b"nodeid")
i += 1
# maybe you want to be extra-sure. in that case you can check the public key generated
# from the private key obtained here against the public key your node is advertising
# to everybody.
from lightning import LightningRpc
from os.path import expanduser
ln = LightningRpc(expanduser("~/.lightning/lightning-rpc"))
i = 0
while True:
salt = bytes([i])
key = Hkdf(salt, hsm_secret, hash=hashlib.sha256).expand(b"nodeid")
privkey = secp256k1.PrivateKey(key)
if privkey.pubkey.serialize().hex() == ln.getinfo()["id"]:
# success!
i += 1
# to generate a custom invoice we'll be using code copied
# from rustyrussel's
# modifications include
# - deleting all decode-related code, as we're only interested in encoding;
# - deleting fallback address stuff, as we're not going to waste blockchain space;
# - adding support for the 'v' tag, for
from lnaddr import lnencode, LnAddr
# first thing, generate a random preimage and hash
import random
import string
import hashlib
from binascii import unhexlify
preimage = "".join([random.choice(string.hexdigits) for _ in range(64)])
print("preimage", preimage)
payment_hash = hashlib.sha256(unhexlify(preimage)).hexdigest()
print("payment_hash", payment_hash)
# now we need the node private key in hex format
# get it using the other file,
import sys
private_key = "" or sys.argv[-1]
# oh, the amount, this is important
amount_msat = 1000000
amount_btc = amount_msat / (1000 * 100000000)
invoice = LnAddr()
invoice.currency = "bc"
invoice.fallback = None
invoice.amount = amount_btc
invoice.paymenthash = unhexlify(payment_hash)
invoice.tags.append(("d", "some random invoice with an lnurl attached"))
invoice.tags.append(("x", "18640"))
invoice.tags.append(("v", ""))
# finally encode the invoice, signed with the node key
bolt11 = lnencode(invoice, private_key)
print("bolt11", bolt11)
# now we must create this same invoice in the node, so it'll be ready
# for it if a payment comes
from os.path import expanduser
from lightning import LightningRpc
ln = LightningRpc(expanduser("~/.lightning/lightning-rpc"))
res = ln.invoice(
msatoshi=amount_msat, # should be the same amount -- however in msatoshis here
label="".join([random.choice(string.ascii_lowercase) for _ in range(8)]),
description="this will be ignored",
preimage=preimage, # this is important
# we can ignore the bolt11 generate here and give the previous one to the payer.
# the node will accept any payments that match the preimage/payment_hash and amount.
# but we can check if the payment_hash is correct here, if you want
print("payment_hash", res["payment_hash"])
#! /usr/bin/env python3
from bech32 import bech32_encode, CHARSET
from binascii import hexlify, unhexlify
from decimal import Decimal
import bitstring
import hashlib
import secp256k1
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
return str(amount) + unit
# 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:
return ret
# Tagged field containing BitArray
def tagged(char, l):
# Tagged fields need to be zero-padded to 5 bits.
while l.len % 5 != 0:
return (
"uint:5, uint:5, uint: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 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)
amount = addr.currency if addr.currency else ""
hrp = "ln" + amount
# Start with the timestamp
data = bitstring.pack("uint:35",
# 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
+ bitstring.BitArray(channel)
+ bitstring.pack("intbe:32", feebase)
+ bitstring.pack("intbe:32", feerate)
+ bitstring.pack("intbe:16", cltv)
data += tagged("r", route)
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)
elif k == "v": # lnurl
data += tagged_bytes("v", v.encode())
# FIXME: Support unknown tags?
raise ValueError("Unknown tag {}".format(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
): = 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(
", ".join([k + "=" + str(v) for k, v in self.tags]),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.