Skip to content

White paper

Lightning tickerplants: Pay-per-ticker with micropayments on the Lightning network

by Jeremy Lucid

Lightning is a technology designed to scale Bitcoin and other compatible blockchains by enabling high transaction throughput with greater privacy while preserving decentralized qualities. It is a layer two infrastructure which builds upon the security and smart contract functionality of the underlying base blockchain, analogous to how the HTTP application layer protocol builds on an underlying and reliable TCP layer. Lightning succeeds by allowing payments to be made off-chain through the technology of bidirectional payment channels, wherein the underlying network of nodes do not need to validate and record every transaction. Consequently, peer-to-peer payments made over the Lightning network can be performed in high volume, with micro value (order of cents and lower), with low or zero fees and with near instant settlement times. Today, Lightning is one of the most rapidly growing networks (see Real-time Network Statistics) and adopted technologies in the cryptocurrency space and is at the cutting edge of blockchain innovation.

Lightning application (LApp) development is progressing quickly and includes eCommerce integrations, micropayment paywalls for content creators, micropayment tipping services (Twitter), and multiple Custodial and Non-Custodial wallet implementations. The micropayment application, in particular, has the potential to transform how online content is monetized by facilitating a micro fee pay-per-view model, as opposed to an ad based or yearly subscription model. Lightning payments are also highly applicable to the IoT space, as the network can be used to implement a decentralized peer-to-peer payment layer for transactions between IoT devices, utilizing all of the networks key features, see IoT and Lightning, Bitcoin Payment-Channels for Resource Limited IoT Devices and Micropayments between IoT devices.

For cryptocurrency exchanges, integrating Lightning has the advantage of allowing clients to more rapidly deposit and withdraw funds, or move funds seamlessly between exchanges. Increasing the velocity of value transfer should, in turn, lead to greater market efficiency and reduce arbitrage opportunities. The exchange ZebPay (link restricted to certain countries) has become the first to begin integrating the payment system. Lightning can also enable exchanges to monetize market data in a completely new way, as showcased in a recent Suredbits POC application, where streaming futures data from the BitMEX and Kraken exchanges was made available to users on-the-fly with Lightning micropayments.

This paper will explore Lightning network technology in the context of its application to the monetization of streaming data. As an example of how Lightning can be integrated into kdb+ based applications, this paper will illustrate how a kdb+ tickerplant can be easily modified to communicate with a Lightning node to accept payments for market, or sensor data, on a per request (ticker) basis, with a fast settlement. In particular, the paper will describe how the kdb+ qlnd library can be used to communicate with a Lightning node to

  • create payment channels with peers
  • generate invoices for payment
  • route payments rapidly across the network.

The paper will also discuss briefly how this setup can be extended to the case of multiple IoT devices exchanging data for payment.

All tests were made using the following software versions.

software version
kdb+ 3.5
Python 3.7.0
Bitcoin Core daemon 0.17.1.0 (gef70f9b52b851c7997a9f1a0834714e3eebc1fd8)
Lightning daemon (lnd) 0.6.0-beta (commit=v0.6.0-beta-41-g1c22474ad31b5f7fe18f9cc8df7c08cd445eaacb)

Optional software

qbitcoind is a q library designed to interact with a Bitcoin core full node.

This library is used herein to transfer funds from a Bitcoin core wallet to a Lightning wallet. For more information on running a node, and using qbitcoind, see Kdb+ Securing Digital Assets and Storing and exploring the Bitcoin blockchain.

The reader should be aware that the above software is a beta version and the technology is still relatively new and experimental. Keep funds held on Lightning to a minimum.

Payment channels

One of the primary building blocks of the Lightning network is bidirectional (two-way) payment channels. Payment channels allow users to make millions of Bitcoin transactions without broadcasting all of them to the Bitcoin network.

A payment channel is constructed between two Lightning network peers, or nodes, by creating a 2-of-2 multisignature address on the Bitcoin blockchain which requires both participants signatures for funds to be spent. This first on-chain transaction determines the balance (or capacity) of the channel and is referred to as the funding transaction. (See Opening a channel.)

In the diagram below, Alice opens a channel to Bob with a channel capacity of 1.1 BTC. The opening balance is 1.0 BTC on Alice's end and 0.1 BTC on Bob’s end. The 1.0 BTC on Alice’s end is referred to as Alice’s outbound capacity and is the amount she is able to spend, or send to Bob. The 0.1 BTC on Bob’s end of the channel is referred to as Alice’s inbound capacity. This inbound capacity determines how much Alice can receive.

Once this funding transaction is confirmed by the Bitcoin network, both participants are then free to transact instantly by exchanging mutually signed commitment transactions that modify the initial balance of the channel. (See Making a payment.) For example, Alice can send 0.1 BTC to Bob over Lightning, updating their respective balances, as shown below. These commitment transactions are not broadcast to the Bitcoin blockchain, allowing thousands of such transactions to be performed per second without incurring a mining fee. The transaction settlement speed is only limited by the time needed by the parties to create, sign and send each other commitment transactions. While the Bitcoin blockchain can process anywhere between 3-7 transactions per second, the Lightning network allows for millions of transactions per second using this approach.

Channels can be closed between peers at any time. At which point, the most recent transaction specifying the latest balances is broadcast to the Bitcoin network. This is known as the settlement transaction, and is when the funds held on the multisignature address are spent to the wallet addresses of the participants. See Closing a channel.

While single-payment channels between pairs of peers are very useful, the major innovation of the Lightning network is enabling payments to be routed between parties who do not have a direct bidirectional payment channel between them, by passing payments along a network of chained payment channels. This is achieved by the use of smart contract technology, namely HTLC (Hash-TimeLock-Contracts), which ensures funds can be transferred in a secure way. For example, if a customer wishes to make a retail payment, but doesn’t have a direct channel open with the retailer, they can instead route the payment along a network of connected channels. Below is an illustration of this idea, where Alice has a choice of multiple payment paths to Bob. A real-world example of a Lightning payment being routed through intermediate nodes can also be seen in the Appendix – Shopping.

The following image is taken from a Lightning node explorer and shows the geographic distribution of public Lightning nodes and the known channels between them. The network has seen dramatic growth over the past year, with the value held on Lightning increasing continuously.


_World map of the Lightning network. Source: https://explorer.acinq.co _

For a more detailed explanation of how Lightning works see Payment Channels Explained and Lightning Network Resources.

Installing and configuring a Lightning node

There are currently multiple implementations of the Lightning protocol, including lnd from Lightning Labs, eclair from ACINQ and c-lightning from Blockstream.

To ensure interoperability between implementations, the community of developers have created the Basis of Lightning Technology (BOLT) specification. This enables development teams to work and specialize on different aspects of the technology, like mobile integration, browser plugins, and enterprise products, while retaining cross-compatibility.

While multiple Lightning implementations exist, the qlnd library discussed here is designed specifically to communicate with the lnd daemon from Lightning Labs. Therefore, the steps described below correspond only to the installation of this implementation.

The steps are broken down into the following parts

  • Installation of the Bitcoin Core Full node, bitcoind
  • Installation of the lnd daemon
  • Running lnd and creating a wallet

Installing bitcoind

In order to run lnd, it is required that a Bitcoin full-node daemon is running and available, preferably, on the same host. This is because the Lightning node needs a way to communicate with the underlying blockchain in order to send on-chain payments, create channel open/close transactions and monitor relevant transactions on the network. While there are novel approaches which do not require a local full node, such as Neutrino, these are outside the scope of this paper.

Detailed steps on how to install and run bitcoind, for different operating systems, can be found on the recommended instructions page.

For Linux-based operating systems, the install procedure can be as simple as running

$sudo apt-get install bitcoind

Before starting the daemon, a bitcoin.conf file should be created in the install folder (usually $HOME/.bitcoin), as described in white paper Storing and exploring the Bitcoin blockchain. However, the sample bitcoin.conf file presented in that white paper should now be extended, as shown below, to include the ZeroMQ wrapper, which will allow the Lightning node to be notified of events like the arrival of new blocks or transactions. Note that in the configuration file below, the rpcuser and rpcpassword values need to be changed.

# Maintain a full transaction index, used to query the node historically.
txindex=1
# [rpc]
# Accept command line and JSON-RPC commands.
server=1
rpcuser=<username>
rpcpassword=<password>
# Additional lines to enable ZeroMQ
zmqpubrawblock=tcp://127.0.0.1:28332
zmqpubrawtx=tcp://127.0.0.1:28332

After starting bitcoind, using the command below, syncing the entire blockchain may take up to one week depending on connectivity. Your Lightning node should not be started until after the Bitcoin node is in sync.

$ bitcoind -daemon

One way to confirm the local node is in sync with the rest of the network is by comparing the block height with another public node. In the example below, a comparison is made against blockexplorer.com.

# Extract the block height from blockexplorer.com
$ wget -q -O- https://blockexplorer.com/q/getblockcount; echo
{"info":{
    "version":120100,
    "protocolversion":70012,
    "blocks":563793,
    "timeoffset":-1,
    "connections":26,
    "proxy":"",
    "difficulty":6061518831027.271,
    "testnet":false,
    "relayfee":0.0001,
    "errors":"Warning: unknown new rules activated (versionbit 1)",
    "network":"livenet"
}}

# Extract the block height from bitcoind using the bitcoin-cli utility
$ bitcoin-cli getblockcount
563793

## Extract the block height from bitcoind using the qbitcoind library 
$q
q).utl.require"qbitcoind"
q).bitcoind.getblockcount[]
result| 563793
error |
id    | 0

Installing lnd

To install and run lnd and its dependencies, follow the official Lnd-Guide.

Once this installation process is complete, create your own Lightning Network configuration file.

Start by creating the config file in the default location:

mkdir ~/.lnd && cd ~/.lnd
touch lnd.conf

Open the lnd.conf file in your favorite editor and add the following details. The alias and color values can be freely chosen by the node operator and can be used to distinguish and find the node on various public node explorers. For additional configuration file options and details, see the following sample file.

[Application Options]
debuglevel=info
alias=<Fill with sudo name. This name helps identify the node on the network>
color=<Fill with a color value given in hex format, for example, #00FF00>

datadir=~/.lnd/data
logdir=~/.lnd/logs
tlscertpath=~/.lnd/tls.cert
tlskeypath=~/.lnd/tls.key

#Specify the interfaces to listen on for p2p connections
listen=<localhost:9735>
externalip=<localhost:9735>

#Specify the interfaces to listen on for REST connections.
restlisten=0.0.0.0:8080

#Specify the interfaces to listen on for gRPC connections
rpclisten=<localhost:10009>

[Bitcoin]
bitcoin.active=1
bitcoin.mainnet=1
bitcoin.node=bitcoind

[Bitcoind]

Running a Lightning node

Once lnd is installed, it can be started by running the following command. Below we explicitly give the path to the lnd.conf file, however, by default lnd will look for it in the home directory if not specified.

$cd $GOPATH/bin
$./lnd --configfile=~/.lnd/lnd.conf

It is possible to run multiple instances of lnd, all connected to the same bitcoind to assist in testing. In this case, a separate lnd.conf file can be created for each instance in a separate folder, being careful to change the listen, externalip, restlisten, rpclisten, alias, color, datadir, logdir, tlscertpath, tlskeypath values as appropriate, and starting the nodes as follows

$./lnd --configfile=~/.lnd1/lnd.conf
$./lnd --configfile=~/.lnd2/lnd.conf
$./lnd --configfile=~/.lnd3/lnd.conf

For more information on possible command-line arguments see the help option.

$./lnd --help

Creating a wallet

During node startup the following output will appear, requesting the user to either create a new wallet or unlock an existing wallet.

$./lnd --configfile=~/.lnd/lnd.conf
Attempting automatic RPC configuration to bitcoind
Automatically obtained bitcoind's RPC credentials
...
2019-03-02 13:47:28.952 [INF] LTND: Waiting for wallet encryption password. 
    Use `lncli create` to create a wallet, 
    `lncli unlock` to unlock an existing wallet,
    or `lncli changepassword` to change the password of an existing wallet 
    and unlock it.

At this point, you will need to run lncli create and follow the instructions to generate a new wallet. Below is the explicit command listing the lnddir and rpcserver which may also be required.

$./lncli --lnddir=~/.lnd --rpcserver=localhost:10009 create
Input wallet password:
Confirm wallet password:

Do you have an existing cipher seed mnemonic you want to use? (Enter y/n): n

Your cipher seed can optionally be encrypted.
Input your passphrase if you wish to encrypt it 
(or press enter to proceed without a cipher seed passphrase):

Generating fresh cipher seed...

!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO RESTORE THE WALLET!!!

---------------BEGIN LND CIPHER SEED---------------
 1. xxxxx      2. xxxxx    3. xxxxx     4. xxxxx
 5. xxxxx      6. xxxxx    7. xxxxx     8. xxxxx
 9. xxxxx     10. xxxxx   11. xxxxx    12. xxxxx
13. xxxxx     14. xxxxx   15. xxxxx    16. xxxxx
17. xxxxx     18. xxxxx   19. xxxxx    20. xxxxx
21. xxxxx     22. xxxxx   23. xxxxx    24. xxxxx
---------------END LND CIPHER SEED-----------------

Be sure to record the 24-word seed, which is essential for wallet recovery.

Interacting with Lightning using qlnd

The qlnd library enables a q process to communicate with a locally running lnd node via the LND REST API. Moreover, the library makes use of the powerful embedPy interface, recently released by KX, which allows the kdb+ interpreter to manipulate Python objects, call Python functions and load Python libraries. This is particularly useful for this application given that the REST API Reference documentation has explicit and well-tested examples using Python. (There are many other applications of embedPy and kdb+.)

EmbedPy setup

EmbedPy is available on GitHub to use with kdb+ V3.5+ and Python 3.5 or higher, for macOS or Linux operating systems and Python 3.6 or higher on the Windows operating system. The installation directory also contains a README about embedPy, and an example directory containing thorough examples. You are encouraged to follow the online documentation to become familiar with the functionality.

Running qlnd

Once embedPy is installed, the qlnd library can be loaded into a q process as follows.

$q qlnd.q

However, prior to loading, you may need to set the LND_DIR environmental variable to the location of your .lnd.conf file, if it is not in the default location $HOME/.lnd.

  export LND_DIR=/path/to/my/.lnd

During library loading, this environment variable is used to locate and read the TLS certificate and Macaroon token created by lnd on startup, which are used for secure communication and authentication with the node, respectively.

By default, the qlnd.q script tries to load the admin.macaroon, which gives full API access without caveats. For applications requiring lower privileged access, an invoice.macaroon and readonly.macaroon are also available, see Macaroons Guide.

In order to change the values of the lnd URL, TLS Certificate path or Macaroon token path post script loading, the following methods are available.

$q qlnd.q
q).lnd.setURL "https://localhost:8080/v1/"
q).lnd.setTLS "/new/path/to/.lnd/tls.cert"
q).lnd.setMACAROON "/new/path/to/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"

To confirm that everything is set up correctly, run .lnd.getInfo to return some basic information from the node.

q).lnd.getInfo[][`version]
"0.6.0-beta commit=v0.6.0-beta-41-g1c22474ad31b5f7fe18f9cc8df7c08cd445eaacb"

q).lnd.getInfo[][`identity_pubkey]
"This returns the public key identifier which is unique to the node"

Funding a Lightning wallet

With the node running, and the qlnd functions returning as expected, the first step towards creating a payment channel is to fund the Lightning wallet with Bitcoin. To do this, instruct the wallet to first generate a new address with .lnd.newaddress.

q).lnd.newaddress[]
address| "bc1qajll8zl8ycflv42rczj5erpt83vzr2ky429t73"

Next, send some funds to this address using a mobile, hardware or exchange wallet of choice. Alternatively, the .bitcoind.sendtoaddress function within the qbitcoind library can be used to transfer funds directly from the internal bitcoind node wallet.

q)toAddr:"bc1qajll8zl8ycflv42rczj5erpt83vzr2ky429t73"
q)amount:0.0106
q).bitcoind.sendtoaddress[toAddr;amount]
result|"df9e4987c8ea8a2dd5c41d8677d8151e02e3e69745ae102e75ae9b636c408706"
error | 0n
id    | 0f

To track the status of this wallet funding transaction, we can query either the bitcoind node using .bitcoind.gettransaction, or the lnd node using .lnd.getTransactions, as shown below.

// Confirm the transaction has been confirmed on the Bitcoin network by
// using the transaction ID output from the sendtoaddress function
q)txid:"df9e4987c8ea8a2dd5c41d8677d8151e02e3e69745ae102e75ae9b636c408706"
q).bitcoind.gettransaction[txid][`result]
amount            | -0.0106
fee               | -1.57e-05
confirmations     | 2678f

Transaction details can also be confirmed on the Lightning node. Below, all transaction details are first converted into a more convenient kdb+ table format to make for easier selection.

q)t:(uj/) enlist@'.lnd.getTransactions[][`transactions]
q)first select from t where tx_hash like txid
tx_hash          | "d61dafc3436973d0ae3f9e820c661a681ab6074b510b5fd51c6f3ca5ed914a0f"
amount           | "-1001481"
num_confirmations| 7470
block_hash       | "0000000000000000002bb995ed3b5022aa4fd8fe73b8130296c5852634cd345d"
block_height     | 560669
time_stamp       | "1548795246"
dest_addresses   | ("bc1q3zy2zdyp77er7rc40xn2udxj888x2qjdun9zdm";"bc1qa4gjgfsuf…")
total_fees       | "1481"

Once enough confirmations are received, the Lightning wallet can be instructed to display the balance by calling .lnd.walletBalance, as shown below. A confirmed balance means the deposit is complete and the node is ready to open channels.

// Funds are reported in Satoshis
q).lnd.walletBalance[]
total_balance    | "1060000"
confirmed_balance| "1060000

Connecting to peers

Before a channel can be opened between Lightning nodes, both nodes need to be able to communicate with one another securely. This can be achieved using the .lnd.connectPeer API. This API requires the user to pass the Lightning node address, which is of the format publickey@host, with a few examples shown below.

Bitrefill           030c3f19d742ca294a55c00376b3b355c3c90d61c6b…@52.50.244.44:9735
LivingRoomOfSatoshi 026b105ac13212c48714c6be9b11577a9ce10f10e1c…@52.224.178.244:9735
PeerNode            02e7c42ae2952d7a71398e23535b53ffc60deb269ac…@93.123.80.47:9735
BitMEX Research     0395033b252c6f40e3756984162d68174e2bd8060a1…@86.162.11.249:9735

Nodes can be found by browsing the node directory available at the various explorer services listed below. These explorers are akin to phone books for public Lightning nodes.

For the purposes of this paper, a connection will be made with the node whose alias is TICKERPLANT, and whose details are as follows.

TICKERPLANT     023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8…@217.160.185.97:9736

To open a connection with the TICKERPLANT node, listed above, pass .lnd.connectPeer a dictionary with keys addr and perm, where

addr

A dictionary with keys pubkey and host, corresponding to the public key and host address

perm

A boolean, where a value of 1b instructs the daemon to persistently connect to the target peer, whereas 0b will be synchronous and timeout if the node is not available

q)tp_pubkey:"023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c" 
q)host:"217.160.185.97:9736"
q).lnd.connectPeer[`addr`perm!(`pubkey`host!(tp_pubkey;host);1b)]

Confirm the node was added as a connection by searching the list of connected peers using .lnd.listPeers. Below, the peer details are first converted into a more convenient kdb+ table format to make for easier selection.

q)t:(uj/) enlist each .lnd.listPeers[][`peers]
q)first select from t where pub_key like tp_pubkey
pub_key   | "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
address   | "217.160.185.97:9736"
bytes_sent| "2163151"
bytes_recv| "2120034"
inbound   | 0b
ping_time | "476"

Opening a channel with a funding transaction

To open a channel with the now connected TICKERPLANT lnd node, we can use the .lnd.openChannel API.

In the example below, a channel can be opened by passing a dictionary with the following key-value pairs.

node_pubkey_string

Hex-encoded pubkey of the node to open a channel with

local_funding amount

The number of satoshis the wallet should commit to the channel

push_sat

Number of satoshis to push to the remote side as part of the initial commitment state

This option is useful in cases where the connecting party wishes to make a payment at the same time as the channel is being opened.

private

Whether this channel should be private, not announced to the greater network

q)d:()!()
q)d[`node_pubkey_string]:tp_pubkey
q)d[`local_funding_amount]:1000000
q)d[`push_sat]:2000
q)d[`private]:0b
q).lnd.openChannel d
funding_txid_bytes| "D0qR7aU8bxzVXwtRSwe2GmgaZgyCnj+u0HNpQ8OvHdY="
output_index      | 1f

In order to track the funding transaction on a block explorer, the funding_txid_bytes value (above) needs to be converted to a base32 txid. The library provides a convenience function to perform this conversion, .lnd.decodeTxid.

q).lnd.decodeTxid["D0qR7aU8bxzVXwtRSwe2GmgaZgyCnj+u0HNpQ8OvHdY="]
"d61dafc3436973d0ae3f9e820c661a681ab6074b510b5fd51c6f3ca5ed914a0f"

This ID can then be tracked on a block explorer or, as shown previously, by using .lnd.getTransactions.

Opening a channel is an on-chain event, so it may take a few confirmations before the channel is open and ready for use. In the meantime, the channel details can be observed using .lnd.pendingChannels.

q).lnd.pendingChannels[][`pending_open_channels][`channel]

Once the channel is opened, its details should appear on the list of opened channels maintained by the node. This list can be accessed using the .lnd.listChannels API. This is a good way to keep track of the local and remote balances associated with the channel.

q)t:(uj/) enlist@'.lnd.listChannels[][`channels]
q)select from t where remote_pubkey like tp_pubkey

Creating an invoice

In order to receive payment for a service, a Lightning network invoice should be generated by the payee and sent to the payer. The .lnd.addInvoice API provides a simple way to generate invoices. In the example below, an invoice is generated by the TICKERPLANT lnd node, by passing the function a dictionary containing the following information.

memo

A simple message which can be used as a reference for the payment

value

The amount in Satoshis the payer should send

expiry

Payment request expiry time in seconds; the default is 3600 (1 hour)

q)d:`memo`value`expiry!("Data Request Received: Pay 100Sat to receive";100;"3600")
q).lnd.addInvoice d
r_hash         | "IIkFKbonG4kD3ih6qlUntfFa4fXLlzEZ7Wx0+7Ek8Bo="
payment_request| "lnbc1u1pwfvnzspp5yzys22d6yudcjq779pa254f8khc44c04ewtnzx0dd360hv…"
add_index      | "29"

The resulting payment_request string contains all the information the payer needs to send a payment.

The following sample invoice was generated by a Lightning-enabled e-commerce website. Here the payment_request string is presented along with its associated QR code.

Making a payment with a commitment transaction

Once the payer has received the payment_request string, the message can be decoded using .lnd.decodePayReq, as shown below.

q)paymentRequest:"lnbc1u1pwfvnzspp5yzys22d6yudcjq779pa254f8khc44c04ewtnzx0dd360hv…"
q).lnd.decodePayReq[paymentRequest]
destination | "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
payment_hash| "20890529ba271b8903de287aaa5527b5f15ae1f5cb973119ed6c74fbb124f01a"
num_satoshis| "100"
timestamp   | "1553353808"
expiry      | "3600"
description | "Data Request Received: Pay 100Sat to receive"
cltv_expiry | "144"

If the payer is satisfied with the invoice details, the .lnd.sendPayment API can be used to pay the invoice over Lightning. The payment below settles in milliseconds.

q)show d:.lnd.sendPayment[(enlist `payment_request)!(enlist paymentRequest)]
payment_preimage| "DjqayRRXI3DRlMlEc2nxl7fLEUaQCNJeDD8WVZD8dZ4="
payment_route   | `total_time_lock`total_amt`hops`total_amt_msat!(568597;"100";+`ch..
payment_hash    | "IIkFKbonG4kD3ih6qlUntfFa4fXLlzEZ7Wx0+7Ek8Bo="

q)d[`payment_route]
total_time_lock| 568597
total_amt      | "100"
hops           | +`chan_id`chan_capacity`amt_to_forward`expiry`amt_to_forward_ms..
total_amt_msat | "100000"

q)d[`payment_route;`hops]
chan_id            | "625016285429039105"
chan_capacity      | "900000"
amt_to_forward     | "100"
expiry             | 568597
amt_to_forward_msat| "100000"
pub_key            | "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600…"

Both parties can continue making payments back and forth on this channel without any additional data footprint on the underlying Bitcoin blockchain. This ability to create off-chain transactions, secured by the underlying base layer, helps scale transactions dramatically and gives increased privacy to channel participants.

Creating a Lightning-enabled tickerplant

To demonstrate how Lightning payments could be integrated into kdb+ based applications, below is an example which modifies a vanilla tickerplant, an application most kdb+ developers are familiar with.

Kdb+ tickerplants are high-performance processes designed for the consumption of real-time streaming data and the publishing of that data to multiple downstream subscribers. Tickerplants operate within a pub/sub messaging model where subscriber processes request data from the tickerplant on a per topic basis and receive all messages relating to that topic, enabling event-driven architectures.

A sample vanilla implementation of tickerplant subscribe-and publish-logic can be found at kdb-tick, and can be started easily from the command line using

q tick.q tableSchemas . -p 5010

For this example, the tableSchemas.q file, which should be present in the tick folder, will contain the following table definition

trade:([] 
  time:`timespan$(); 
  sym:`symbol$(); 
  price:`float$(); 
  size:`long$() 
 )

The main library functions this tickerplant implementation uses are shown below, and they will form the basis for subsequent modifications.

White paper: Building Real-Time Tick Subscribers

\d .u
init:{w::t!(count t::tables`.)#()}

del:{w[x]_:w[x;;0]?y};.z.pc:{del[;x]each t};

sel:{$[`~y;x;select from x where sym in y]}

pub:{[t;x]{[t;x;w]if[count x:sel[x]w 1;(neg first w)(`upd;t;x)]}[t;x]each w t}

add:{
  $[(count w x)>i:w[x;;0]?.z.w; .[`.u.w; (x;i;1); union; y]; w[x],:enlist(.z.w;y)];
  (x;$[99=type v:value x;sel[v]y;0#v])}

sub:{if[x~`;:sub[;y]each t];if[not x in t;'x];del[x].z.w;add[x;y]}

end:{(neg union/[w[;;0]])@\:(`.u.end;x)}

In the standard setup, subscribers make a synchronous request to the tickerplant, calling the .u.sub function and specifying their request topic. The .u.sub function takes two arguments, a table name and list of symbols, where for market data the symbols usually correspond to ticker symbols. For example, below a subscriber opens a handle to a tickerplant which is listening on port 5010 and requests data from the trade table for ticker symbols AAPL and GOOGL.

q)h:hopen 5010;
q)h".u.sub[`trade;`AAPL`GOOGL]"
`trade
+`time`sym`price`size!(`timespan$();`g#`symbol$();`float$();`long$())

Above, the subscriber is returned a two-item list containing the table name and the table schema which the subscriber should define in order for records to be received correctly and immediately. The subscriber details are registered on the tickerplant within the .u.w dictionary, which stores the users handle value and their request information, as shown below.

q).u.w
trade| 7i `AAPL`GOOGL

If the subscriber has set the table definition, along with a upd function (upd:insert) the data will be received for only the data subscribed to. A sample feed handler which pushes mock market data to this tickerplant can be found at feed.q

Diagrammatic overview

The diagram below shows the high-level setup for enabling payments between a subscriber and tickerplant process. Both the subscriber and tickerplant are communicating with their own lnd nodes, highlighted in blue. In this case, a direct channel is opened between the tickerplant and subscriber node for near-instant and fee-less payments; however, a direct channel is not required. Transactions can also be made where payments are routed via intermediate nodes which connect both, highlighted in grey.

Subscribe-pay-publish

The integration of Lightning will modify the standard sub/pub model to a sub/pay/pub model where subscribers requests for data will be enabled only after a Lightning payment is received. For this, the first step will be to modify the .u.sub function such that it returns a Lightning payment invoice to the subscriber indicating the amount to pay in satoshis for the data requested. While this payment is pending, the users request details will be stored in a table called .u.pendingInvoices before ultimately being added to the .u.w dictionary, shown previously.

pendingInvoices:([] 
 handle:`int$();
 request:();
 index:`long$();
 settled:`boolean$()
 )
sub:{[tableName;symList]
  if[tableName~`;
    :sub[;symList] each t
  ];

  if[not tableName in t;
   'tableName
  ];

  del[tableName].z.w;
  delete from `.u.pendingInvoices where handle in .z.w;
  add[tableName;symList]
 }

 add:{[tableName;symList]
  memo:"Invoice:",.Q.s (!). enlist@'r:(tableName;symList);
  invoice:.lnd.addInvoice[`memo`value`expiry!(memo;100*count symList;3600)];
  insert[`.u.pendingInvoices;enlist@'(.z.w;r;"J"$invoice[`add_index];0b)];
  (tableName;$[99=type v:value tableName;sel[v]symList;0#v];invoice)
 }

In the above .lnd.addInvoice call, the amount argument is being determined by a simple calculation whereby the number of symbols being requested is multiplied by 100 satoshis. A small memo message is also being derived along with a request time of 1 hour.

With the above modifications, the response message returned from a synchronous call to .u.sub contains a third element, the Lightning payment invoice.

q)h:hopen 5010
q)result:h".u.sub[`trade;`AAPL`GOOGL]"
q)result
`trade
+`time`sym`price`size!(`timespan$();`g#`symbol$();`float$();`long$())
`r_hash`payment_request`add_index!("QZKFOR+nzeHw…";"lnbc20n1pwxspr…";"27")

As shown previously, the subscriber can decode this invoice to see the payment details in plain text.

q).lnd.decodePayReq (result[2])[`payment_request]
destination | "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
payment_hash| "419285391fa7cde1f0bf9de83e59d94e664fecb996298de7e11d7ec2d7a6838b"
num_satoshis| ,"2"
timestamp   | "1550320751"
expiry      | "3600"
description | "Invoice:trade| AAPL GOOGL\n"
cltv_expiry | "144"

On the tickerplant, the subscribers’ request details have been populated in the .u.pendingInvoices table, however, .u.w is still empty, pending payment.

q).u.pendingInvoices
handle request            index settled
---------------------------------------
7      `trade `AAPL`GOOGL 27    0
q).u.w
trade|

Subscriber: making payment

On the subscriber end, making a payment is straightforward using the .lnd.sendPayment function.

q)d:(enlist `payment_request)!(enlist (result[2])[`payment_request])
q)result:.lnd.sendPayment d
q)result
payment_preimage| "EJClS1fuq9owXAe7rpgdjuyMPsIvNegsPLZ6Wz38cnw="
payment_route   | `total_time_lock`total_amt`hops`total_amt_msat!(563411f;,"2";+`_..
payment_hash    | "QZKFOR+nzeHwv53oPlnZTmZP7LmWKY3n4R1+wtemg4s="

The list of all past transactions can also be accessed using .lnd.listPayments.

q)select value_sat, path from .lnd.listPayments[][`payments]
value_sat path
------------------------------------------------------------------------------
"100"     "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"8"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"1"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"1"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"1"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"1"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"1"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"2"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"1"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"2"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"
,"2"      "023bc00c30acc34a5c9cbf78f84aa775cb63f578a69a6f8ec9a7600753d4f9067c"

Tickerplant: confirming payment

The Lightning node API provides a SubscribeInvoices option which returns a uni-directional stream from the lnd server to the client, which can be used to notify the client of newly added/settled invoices. By subscribing to this stream the tickerplant can be notified immediately of payment. However, because this is a blocking call, we will instead use a dedicated q process to listen and push events onto the tickerplant immediately.

Below is shown the embedPy script and command to run this listener process. (See invoiceListener.q). Notice that the portnumber (tickerplant), authHeader, url and TLS cert values have been hardcoded and should be changed if necessary. A listening process like this one would only require a (least privilege) invoice.macaroon token when making requests so that access is limited to only invoice related APIs.

p)import base64, codecs, json, requests, os
p)from qpython import qconnection
p)url = 'https://localhost:8080/v1/'
p)LND_DIR = os.getenv('LND_DIR', os.getenv('HOME')+'/.lnd')
p)cert_path =  LND_DIR+'/tls.cert'
p)macaroon = codecs.encode(open(LND_DIR+'/data/chain/bitcoin/mainnet/invoice.macaroon', 
    'rb').read(), 'hex')
p)headers = {'Grpc-Metadata-macaroon': macaroon}

p)q = qconnection.QConnection(host='localhost', port=5010)
p)q.open()

p)def listener(queryParameters=''):
  endpoint  = 'invoices/subscribe'+queryParameters
  r = requests.get(url+endpoint, headers=headers, verify=cert_path, stream=True)
  for raw_response in r.iter_lines():
      json_response = json.loads(raw_response)
      print("Invoice message event received")
      print(raw_response)
      q('.u.processInvoices', raw_response)

q).lnd.listener:.p.get[`listener;<]
q).lnd.listener[]

The program can be run using the following command. The first message is an invoice creation, generated when the tickerplant calls .lnd.addInvoice.

$q invoiceListener.q

Invoice message event received: 
    {"result":
        {"memo":"Invoice:trade| AAPL GOOGL\n",
         "r_preimage":"EJClS1fuq9owXAe7rpgdjuyMPsIvNegsPLZ6Wz38cnw=",
         "r_hash":"QZKFOR+nzeHwv53oPlnZTmZP7LmWKY3n4R1+wtemg4s=",
         "value":"2",
         "creation_date":"1550320751",
         "payment_request":"lnbc20n1pwxspr0pp5gxfg2wgl5lx7ru9lnh5rukwefeny…",
         "expiry":"3600",
         "cltv_expiry":"144",
         "add_index":"27"
        }
    }

The second message is an invoice settlement, generated after the subscriber calls .lnd.sendPayment and the payment is confirmed by the tickerplant node.

Invoice message event received: 
    {"result":
        {"memo":"Invoice:trade| AAPL GOOGL\n",
         "r_preimage":"EJClS1fuq9owXAe7rpgdjuyMPsIvNegsPLZ6Wz38cnw=",
         "r_hash":"QZKFOR+nzeHwv53oPlnZTmZP7LmWKY3n4R1+wtemg4s=",
         "value":"2",
         "settled":true,
         "creation_date":"1550320751",
         "settle_date":"1550320834",
         "payment_request":"lnbc20n1pwxspr0pp5gxfg2wgl5lx7ru9lnh5rukwefeny…",
         "expiry":"3600",
         "cltv_expiry":"144",
         "add_index":"27",
         "settle_index":"11",
         "amt_paid":"2000",
         "amt_paid_sat":"2",
         "amt_paid_msat":"2000",
         "state":"SETTLED"
        }
    }

Tickerplant: Enabling subscriber

Of note in the above code is the synchronous call to the kdb+ tickerplant which executes the function .u.processInvoices with the event message sent from the node.

q('.u.processInvoices', raw_response)

On the tickerplant, this is a simple function which extracts the invoice index number and uses it to take records from .u.pendingInvoices table and adds them to .u.w.

processInvoices:{[x]
  msg:first .j.k x;   // Convert msg (string) into a kdb+ dictionary
  if[`state in key msg; // If state key is present, then invoice is settled
    settledIndex:"J"$msg[`add_index]; // Extract invoice index number
    settled:select from .u.pendingInvoices where index=settledIndex;
    .u.addPayes[;;]'[settled`handle;(settled`request)[;0];(settled`request)[;1]];
    delete from `.u.pendingInvoices where index in settledIndex
  ];
 }

addPayes:{[handle;tableName;symList]
  $[(count w tableName)>i:w[tableName;;0]?handle;
    .[`.u.w;(tableName;i;1);union;symList];
    w[tableName],:enlist(handle;symList)
  ];
 }

Once a settled invoice is received the .u.w dictionary will be updated and the .u.pendingInvoices will be cleared

q).u.w
trade| 7i `AAPL`GOOGL
q)delete from `.u.pendingInvoices
`.u.pendingInvoices
q).u.pendingInvoices
handle request index settled
----------------------------

From this moment on, the subscriber will begin receiving updates.

Closing a channel with a settlement transaction

At any point, either participant in the channel can choose to close it. A channel closing event is an on-chain transaction where the multisig address spends the funds back to each party according to their agreed-upon channel amount. To close a channel you first need to identify the channel point associated with the channel, as shown below.

q)last .lnd.listChannels[][`channels]
active                 | 1b
remote_pubkey          | "02d0d487572a10c1d4dc486f03f09205c657abc471d0f3258ce37b034…"
channel_point          | "d61dafc3436973d0ae3f9e820c661a681ab6074b510b5fd51c6f3ca5e…"
chan_id                | "616462084875288577"
capacity               | "1000000"
local_balance          | "2120"
remote_balance         | "994052"
commit_fee             | "3828"
commit_weight          | "724"
fee_per_kw             | "5288"
total_satoshis_received| "120"
num_updates            | "488"
csv_delay              | 144

The first part of this channel point value, before the colon, is the Bitcoin transaction ID of the funding transaction, and the second part, after the colon, is the index of the transaction output. The transaction and index need to be passed to the .lnd.closeChannel API to close the channel.

q)result:.lnd.closeChannel["d61dafc3436973d0ae3f9e820c661a681ab6074b510b5fd51…";"1"]
q)result
-------------------------------------------------------------------------------
(,`close_pending)!+(,`txid)!,,"qAyOhsqXUmswSCTrF/20YOr93wbXmj4pTTOyK+PA4zg="
(,`chan_close)!+(,`closing_txid)!,,"qAyOhsqXUmswSCTrF/20YOr93wbXmj4pTTOyK+PA4..

As shown previously, the txid value above needs to undergo a conversion in order to retrieve the txid in the appropriate format to enable searching in a block explorer.

q).lnd.decodeTxid["qAyOhsqXUmswSCTrF/20YOr93wbXmj4pTTOyK+PA4zg="]
"38e3c0e32bb2334d293e9ad706dffdea60b4fd17eb2448306b5297ca868e0ca8"

Once the channel close has completed, the wallet balance should display the received amount.

q).lnd.walletBalance[]
total_balance    | "2120"
confirmed_balance| "2120"

In summary, only two on-chain transactions were required, one to open the channel and one to close it. All the intermediate transactions which occurred over the bi-directional payment channel have not hit the blockchain and did not incur any on-chain fees.

Channel management

Currently, one of the main technical challenges within Lightning is around channel management and the maintenance of sufficient inbound and outbound capacity to facilitate payments. Channels can be thought of as being analogous to rechargeable batteries, in that their full value is realized with multiple usages, not with single use, because there is a financial overhead associated with their creation and destruction. There is also a wait time associated with channel creation and channel closing due to the need for multiple on-chain confirmations.

Therefore, proper channel management is required to keep channels opened and balanced to ensure funds can keep flowing bi-directionally. For example, in the case of a subscriber sending payments to a tickerplant via a direct payment channel, eventually all funds will accumulate on the tickerplant (receiving) end of the channel which will prevent the tickerplant side from receiving more funds due to depleted inbound capacity. At this point, either party could close the existing channel and reopen a new channel with a new channel capacity, but this can be avoided by re-balancing channel funds instead.

One way for the receiving side to cash-out accumulated outbound capacity, without closing, is to use a service like loop (blog-post) where channel outbound capacity can be exchanged for on-chain funds while topping up the inbound (or receiving) capacity. Details on how this can be performed are outside of the scope of this paper and, for now, are left to the reader as an exercise. It is hoped that future follow-up papers will explore this aspect in more details.

For further insights into channel-managment best practices see this video.

Extension to multiple IoT devices

The approach followed for the tickerplant and subscriber setup can be extended to the use case of multiple IoT devices that are sending and receiving payments. A set of devices can authenticate and communicate with a single lnd node using just the TLS certificate file and an invoice.macaroon in order to generate invoices for subscribers. From a security perspective, the invoice.macaroon limits lnd node access to just the required functionality. Similarly, the listener process can broadcast invoice settlement messages back to individual devices to release data to subscribers. In this way, individual devices do not need to run their own node or store Bitcoin private keys.

Conclusion

The technology of Bitcoin and layer-two solutions like Lightning open up the possibility for applications to interact directly with a decentralized peer-to-peer payments layer through the use of simple APIs, where the value transfer reduces to the exchange of encoded text messages over TCP/IP.

This ability to easily send and receive payments in a peer-to-peer fashion, especially micropayments, has the potential to enable the construction of new innovative applications not hindered by third-party friction.

In the tickerplant example, a simple template was provided to demonstrate how market data, or any other form of streaming data, could be monetized with the creation of a pay-per-request system utilizing Lightning micropayments.

While Lightning remains an experimental and rapidly changing technology, with many outstanding challenges, it is hoped that this paper has at least helped explain some of the key concepts and techniques, and also showcased some synergies between the technology and kdb+ for potential integrations.

PDF

Author

Jeremy Lucid

Jeremy Lucid is a kdb+ consultant, based in Ireland, who has worked on real-time Best Execution projects for a major multinational banking institution and a Kx for Surveillance implementation at a leading options and futures exchange.

Acknowledgments

I would like to thank the Lightning Development Community for providing their insight and assistance on many technical queries and issues.

Appendix

Setting channel fees

Lightning node operators can charge a fee for routing payments for other peers. The .lnd.updateChannelPolicy API can be used to set the fee rate. For more info on the economics of fees on Lightning, see the recent article from BitMEX research.

.lnd.updateChannelPolicy[`global`fee_rate`time_lock_delta!(1b;1000;6)]

Shopping

Users can test the API by buying small items on the various Lightning-enabled stores. Below is an example of a payment request generated by the Blockstream store, and presented at checkout time to the customer. By decoding the string we can see the amount to be paid and the description of the item being bought, in this case, a $4.99 sticker.

q)paymentRequestFromBlockstream:"lnbc1252532570p1pwf84ldpp557ypsquf8lyvuwrytnjnykqu…"
q).lnd.decodePayReq[paymentRequestFromBlockstream]
destination | "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f"
payment_hash| "a7881803893fc8ce38645ce532581cb3f814593c487a401308d8e0d88445dd84"
num_satoshis| "125253"
timestamp   | "1553192941"
expiry      | "3600"
description | "Blockstream Store: 4.99 USD for \"Don't Trust. Verify.\" Sticker x 1"
cltv_expiry | "10"
route_hints | +(,`hop_hints)!,,+`node_id`chan_id`fee_base_msat`fee_proportional_mi..

As shown previously, payment can be made easily using the .lnd.sendPayment

q)d:(enlist `payment_request)!(enlist paymentRequestFromBlockstream)
q)result:.lnd.sendPayment d

The message returned by the API, for the successful payment, includes the route hop information. Here we see that the payment was routed to the Blockstream lnd node through one intermediate node. This is an example of an indirect payment where funds were routed to the destination through an intermediate node.

q)result[`payment_route][`hops]
`chan_id`chan_capacity`amt_to_forward`fee`expiry`amt_to_forward_msat`fee_msat`pub..
`chan_id`chan_capacity`amt_to_forward`expiry`amt_to_forward_msat`pub_key!("621446..
q)first result[`payment_route][`hops]
chan_id            | "623480267613601793"
chan_capacity      | "1000000"
amt_to_forward     | "125253"
fee                | "13"
expiry             | 568172
amt_to_forward_msat| "125253257"
fee_msat           | "13525"
pub_key            | "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c3665…"