Skip to content

Instantly share code, notes, and snippets.

@brianddk
Last active July 13, 2019 08:37
Show Gist options
  • Save brianddk/1f339832a677aa941c7c16a7aeac8735 to your computer and use it in GitHub Desktop.
Save brianddk/1f339832a677aa941c7c16a7aeac8735 to your computer and use it in GitHub Desktop.
Bitcoin Lightning Fee simulator

Lighting network simulator

This is a very simple python program that will simulate a few conditions. For the ground work lets set up an Alice and Bob story.

Bob's friend Alice sets up a coffee shop. She is going to start taking cryptocurrency at the coffee shop, so she is set up to accept Bitcoin Cash (BCH) and Bitcoin Core via Lightning (BTCLN). Bob has a substantial holding in both BTC and BCH, though it is all in cold storage in a HW wallet. He does have a Coinbase account but is always very careful to calculate trading fees. So now Bob wants to determine what would be the best way to get ready to move his daily coffee patronage to Alice's shop.

Looking over his past expenses on morning coffee, he decides that 20,000 satoshis (BTC) should be a nice round number for lattes and perhaps 19,000 for coffee. Since BTC is trading at $10,600, his budget in terms of fiat comes out to just about $2 a day. With his budget in hand, he goes about reviewing the BCH and BTCLN payment options. He suspects BTC (mainnet) is too costly since he doesn't want to leave Alice with low priority economical transactions to chase.

Bitcoin Cash

His simplest solution of course is just to move BCH from his HW wallet (cold) to his mobile wallet (hot), but he wants to keep his BTC and BCH accounts fairly balanced and doesn't want to deplete his BCH funds entirely. He also wants to pay Alice in both BCH and BTCLN since she seemed excited about her new Point Of Sale system. He had thought about converting some of his BCH to BTC, though he realizes that there is always a cost for this on Coinbase (usually 0.15%).

Eclair LN Wallet

The next wallet Bob looked at was the Eclair mobile LN wallet. This would be simplest to fund with BTC which of course would have costs in moving funds from his HW wallet to Eclair, and then even more costs opening and closing a channel. He also noted on the github for the project that many LN wallets do not allow fees to be set on channel close operations. This might get costly as market fee rates rise.

Wallet of Satoshi, funded by a BTCLN exchange.

Another popular LN solution is Wallet of Satoshi (WoS). WoS seems like a simple solution since it doesn't have the same constraints on incoming and outgoing capacity like Eclair does. There is also no need to do any complicated channel setup since it is custodial and that is all just handled. Bob does realize that WoS funding options might be costly. He researches to ways to load and unload WoS using the FixedFloat exchange as well as the bitrefill Thor channel services.

FixedFloat for BCH funding and withdrawals from WoS

As noted before with WoS, LN transactions are seamless enough, but WoS has no way to convert funds back to BTC. For this we have to use a LN exchange. FixedFloat is a fairly reasonable exchange offering competitive exchange rates and a 0.5% fee. To convert funds from BTC, WoS has offers a comptitive rate of 0.3%, so obviously this is the simplest way to fund the wallet.

Bitrefill for BTC funding

Although not a verify competitive option, the Bitrefill funding option is included here just to see how it stacks up to the others.

Results

I've included two runs of the simluator in LibreOffice format. They should be viewable in Google Sheets, Excel or LibreOffice

Latte (without bugfix)

This simulated buying lattes (20k) and compared the cost of using various wallets. Important inflection points that were found:

  • 17 cups / 3.6 mBTC - If spending less than 3.6 mBTC, it is cheaper to convert the BTC to BCH and just use a BCH wallet. At 17 cups however, using the WoS LN wallet is the cheapest way to spend BTC.
  • 39 cups / 8.0 mBTC - If spending less than 8.0 mBTC, it is cheaper to fund your WoS LN using BCH. At 39 cups it becomes less expensive to use BTC to fund your WoS LN wallet.

Coffee (with bugfix)

This simulated buying coffees (10k) and compared the cost of using various wallets. Important inflection points that were found. This simulation did assume that the expensive channel close bug in Eclair was fixed:

  • 4 cups / 1.9 mBTC - If spending less than 1.9 mBTC, it is cheaper to convert the BTC to BCH and just use a BCH wallet. At 4 cups however, using the WoS LN wallet is the cheapest way to spend BTC.
  • 41 cups / 8.0 mBTC - If spending less than 8.0 mBTC, it is cheaper to fund your WoS LN using BCH. At 41 cups it becomes less expensive to use BTC to fund your WoS LN wallet.
  • 853 cups / 163 mBTC - If spending less than 163 mBTC, the average TXN cost of using BCH is cheaper than using BTCLN. At 853 cups (yikes) however, it is cheaper to use BTCLN for coffee than to use BCH.

Files

  • +README.md - This file (named for sorting)
  • lnsim.py - The python file to run the sims, produces CSV to stdout
  • latte.ods - LibreOffice file of sim run for latte prices (no bugfix applied). Select view-raw to download.
  • coffee-bugfix.ods - LibreOffice file of sim run for coffee prices (w/bugfix). Select view-raw to download.
  • .gitignore - A git artifact, as it suggests... please ignore.

Usage

To run this, do the following:

  1. git clone https://gist.github.com/brianddk/1f339832a677aa941c7c16a7aeac8735 lnsim to get files.
  2. Edit lnsim.py and update the defines at top of the file to your liking
  3. python3 lnsim.py > results.csv or python.exe in windows
  4. Open results.csv in LibreOffice

Tips

Coin Address / QR
BTCLN https://tippin.me/@dkbriand
LTC MKcAge42cX6WZnnPfFGJAxReUYZUbsi6t3
LTC-b32 ltc1q5uucgx9f8n70nq7jmjy03rpg84cm4tm70z5rz6
BCH-b32 qqz77k4rqar3uppj8k28de06narwkqaamcf624p8zl
ETH / BAT 0xBc72A79357Ff7A59265725ECB1A9bFa59330DB4b
__pycache__/
#!/usr/bin/env python3
# [rights] Copyright brianddk 2019 https://github.com/brianddk
# [license] Apache 2.0 License https://www.apache.org/licenses/LICENSE-2.0
# [repo] https://gist.github.com/brianddk/1f339832a677aa941c7c16a7aeac8735
# [bchtip] BCH: bitcoincash:qqz77k4rqar3uppj8k28de06narwkqaamcf624p8zl
# [tipjar] https://gist.github.com/brianddk/3ec16fbf1d008ea290b0
# [refered] https://www.reddit.com/[tbd]
# [usage] python3 lnsim.py > results.csv
class InsufficentFundsError(Exception):
"""Ran out of satoshis for coffee"""
pass
class WalletSim:
"""A superclass for various spend simulators"""
# DEFINES - Edit these to your liking
latte = True # whether to use latte or coffee pricing
bugfix = False # whether the Eclair close bug is fixed
txn_coffee = 19_000 # 19,000 sat is the price of coffee
txn_latte = 20_000 # 20,000 sat is the price of latte
txn_size = txn_latte if latte else txn_coffee
scale = 100_000_000 # 100 million sat = 1 BTC
lo_pri = 1_602 # BTC sat TXN cost for low priority
hi_pri = 17_404 # BTC sat TXN cost for hi priority
bch_btc_hi = 3_769_000 # BTC sat costs for 1 BCH on sell-book
bch_btc_low = 3_760_000 # BTC sat costs for 1 BCH on buy-book
bch_btc_fair= (bch_btc_hi + bch_btc_low) / 2
bch_pri = 226 # BCH sat TXN cost for hi priority
bch_spnd = bch_pri * bch_btc_fair / scale # conv to BTC sat
ln_pri = 1 # LN sat TXN cost for single TXN
cex_fee = 0.0015 # 0.15% fee for limit orders at Coinbase
wos_fee = 0.0030 # 0.30% fee for BTC conversion at WoS
dex_fee = 0.0050 # 0.50% fee for BTC or BCH conv. at FixedFloat
brefill_fee = 0.0200 # 2.00% fee for BTC conv at Bitrefill
reserve = 22_000 # Eclair channel reserve for future fees
# /end DEFINES
def __init__(self, init, txns, rtn, inbch, outbch):
self.spnd_fee = self.ln_pri
self.bal = init
self.init = init
self.txns = txns
self.return_funds = rtn
self.inbch = inbch
self.outbch = outbch
def __str__(self):
selfstr = type(self).__name__
selfstr += ":1" if self.return_funds else ":0"
selfstr += ":1" if self.inbch else ":0"
selfstr += ":1" if self.outbch else ":0"
return selfstr
def spend(self):
self.bal -= self.txns * (self.spnd_fee + self.txn_size)
if self.bal <= 0: raise InsufficentFundsError()
def run_sim(self):
if self.inbch: self.fund_bch()
else: self.fund_btc()
self.spend()
if self.return_funds:
if self.outbch: self.return_bch()
else: self.return_btc()
return self.init - (self.bal + self.txn_size * self.txns)
class Eclair(WalletSim):
"""A Eclair wallet simulator"""
def __init__(self, init, txns, rtn, inbch, outbch):
super().__init__(init, txns, rtn, inbch, outbch)
self.fees = self.run_sim()
def fund_btc(self):
# HW wal to Eclair
self.bal -= self.lo_pri
# Eclair chan open
self.bal -= self.lo_pri
# hold reserve
self.bal -= self.reserve
def fund_bch(self):
# HW-wal to CEX
self.bal -= self.bch_spnd
# CEX Fee
self.bal -= self.bal * self.cex_fee
# Eat spread & send to Eclair
self.bal *= self.bch_btc_low / self.bch_btc_fair
# Eclair chan open
self.bal -= self.lo_pri
# hold reserve
self.bal -= self.reserve
def return_btc(self):
# return reserve
self.bal += self.reserve
# Chan close
self.bal -= self.lo_pri if bugfix else self.hi_pri
# BTC to trez
self.bal -= self.lo_pri
def return_bch(self):
# btc to CEX
self.return_btc()
# CEX Fee
self.bal -= self.bal * self.cex_fee
# Eat spread & send along
self.bal *= self.bch_btc_fair / self.bch_btc_hi
class WosWallet(WalletSim):
"""A WoS based wallet"""
def __init__(self, init, txns, rtn, inbch, outbch):
super().__init__(init, txns, rtn, inbch, outbch)
self.spnd_fee = 2 * self.ln_pri # WoS adds a 1 sat fee to most TXNs
self.fees = self.run_sim()
def return_btc(self):
# WoS to DEX
self.bal -= self.spnd_fee
# DEX Fee
self.bal -= self.bal * self.dex_fee
# DEX to HW-wal
self.bal -= self.hi_pri
def return_bch(self):
# WoS to DEX
self.bal -= self.ln_pri
# DEX Fee
self.bal -= self.bal * self.dex_fee
# DEX to HW-wal
self.bal -= self.bch_spnd
class FixedFloat(WosWallet):
"""A FixedFloat wallet simulator"""
def fund_btc(self):
# HW wal to WoS
self.bal -= self.lo_pri
# WoS conversion fee
self.bal -= self.bal * self.wos_fee
def fund_bch(self):
# HW-wal to DEX
self.bal -= self.bch_spnd
# DEX Fee
self.bal -= self.bal * self.dex_fee
# DEX to WoS
self.bal -= self.ln_pri
class Bitrefill(WosWallet):
"""A Bitrefill wallet simulator"""
def fund_btc(self):
# HW wal to CB
self.bal -= self.lo_pri
# bitrefill fee
self.bal -= self.bal * self.brefill_fee
def fund_bch(self):
# HW-wal to CB
self.bal -= self.bch_spnd
# CEX Fee
self.bal -= self.bal * self.cex_fee
# Eat spread
self.bal *= self.bch_btc_low / self.bch_btc_fair
# bitrefill fee
self.bal -= self.bal * self.brefill_fee
class ChainWallet(WalletSim):
"""A WoS based wallet"""
def btc_to_bch(self):
# HW-wal to CEX
self.bal -= self.lo_pri
# CEX Fee
self.bal -= self.bal * self.cex_fee
# Eat spread & send along
self.bal *= self.bch_btc_fair / self.bch_btc_hi
def bch_to_btc(self):
# HW-wal to CEX
self.bal -= self.bch_spnd
# CEX Fee
self.bal -= self.bal * self.cex_fee
# Eat spread & send along
self.bal *= self.bch_btc_low / self.bch_btc_fair
class Bch(ChainWallet):
"""A BCH wallet simulator"""
def __init__(self, init, txns, rtn, inbch, outbch):
super().__init__(init, txns, rtn, inbch, outbch)
self.spnd_fee = self.bch_spnd
self.fees = self.run_sim()
def fund_btc(self):
self.btc_to_bch()
def fund_bch(self):
# HW-wal to SW-wal
self.bal -= self.bch_spnd
def return_btc(self):
self.bch_to_btc()
def return_bch(self):
# SW-wal to HW-wal
self.bal -= self.bch_spnd
class Btc(ChainWallet):
"""A BTC wallet simulator"""
def __init__(self, init, txns, rtn, inbch, outbch):
super().__init__(init, txns, rtn, inbch, outbch)
self.spnd_fee = self.lo_pri
self.fees = self.run_sim()
def fund_btc(self):
# HW-wal to SW-wal
self.bal -= self.lo_pri
def fund_bch(self):
self.bch_to_btc()
def return_btc(self):
# SW-wal to HW-wal
self.bal -= self.lo_pri
def return_bch(self):
self.btc_to_bch()
if __name__ == "__main__":
txn_size = WalletSim.txn_size
scale = WalletSim.scale
cur = prv = lst = []
maxln = (1 << 24) - 1 # max ln channel per BOLT002
for i in range(5, 5000, 1):
init = i * txn_size
if init > maxln: break
for cls in [Eclair, FixedFloat, Bitrefill, Btc, Bch]:
txns = init // txn_size
for rtn in [True, False]:
for inbch in [True, False]:
for outbch in [True, False]:
if not rtn and outbch:
continue
if not rtn and cls.__name__ == "Eclair":
continue
while txns > 0:
try:
obj = cls(init, txns, rtn, inbch, outbch)
break
except InsufficentFundsError:
txns -= 1
lst.append(obj)
lst.sort(key=lambda x: x.fees)
cur = [str(x) for x in lst]
if prv != cur:
btc = '{:.4f}'.format(init / scale)
bal = btc + "[" + str(txns) + "]"
print(", ".join([bal] + [str(x) for x in lst]))
lst = []
prv = cur
@brianddk
Copy link
Author

brianddk commented Jul 3, 2019

Please leave a comment if you have any questions or corrections.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment