Core lightning rpc_command hook, pay command and BOLT11 invoice
In this live we write a plugin that limits the amount a node can send (using the builtin pay command) to a BOLT11 invoice. This is possible thanks to Core Lightning hook system and specifically the hook rpc_command.
Transcript with corrections and improvements
In this live we write a plugin that limits the amount a node can send
(using the builtin pay command) to a BOLT11 invoice.
Specifically, we write the plugin pay-up-to.py such that when we start
it with the startup option limit set to 0.001btc for instance, we
can't pay invoices higher than that threshold. Here an example where
l1-cli and l2-cli are aliases for lightning-cli command with
--lightning-dir set respectively to /tmp/l1-regtest and
/tmp/l2-regtest:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/pay-up-to.py limit=0.001btc
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv "too expensive pizza"
lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
rpc_command hook
The hook system allows plugins to register to some events that can
happened in lightningd and ask lightningd to be consulted to decide
what to do next.
A list of those events can be found in the documentation.
Regarding the rpc_command hook, when a plugin registers to it, each
time a client send a JSON-RPC request to lightningd, lightningd
forwards it to the plugin and waits for the plugin to tell it what to
do next. The plugin can answer to lightningd in 4 ways:
I don't care about that request do what you were supposed to do,
I modified the request, now do what you were supposed to do but with the modified request,
Take that response and give it to the client,
Take that error and give it to the client.
This can be visualize like this:
(if registered to
rcp_command hook)
sends a forwards the
JSON-RPC request JSON-RPC request
┌───────┐------------------>┌──────────┐------------------------>┌───────────┐
│client │ │lightningd│ │a-plugin.py│
└───────┘<------------------└──────────┘<------------------------└───────────┘
- result.result ("continue")
- result.replace (...)
- result.return.result (...)
- result.return.error (...)
Let's write some code.
Install pyln-client and start 2 Lightning nodes running on regtest
Let's install pyln-client in a Python virtual environment
◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ pip install pyln-client
...
and start two Lightning nodes running on the Bitcoin regtest
chain and check the alias of the command l1-cli:
(.venv) ◉ tony@tony:~/clnlive:
$ source lightning/contrib/startup_regtest.sh
lightning-cli is /usr/local/bin/lightning-cli
lightningd is /usr/local/bin/lightningd
Useful commands:
start_ln 3: start three nodes, l1, l2, l3
connect 1 2: connect l1 and l2
fund_nodes: connect all nodes with channels, in a row
stop_ln: shutdown
destroy_ln: remove ln directories
(.venv) ◉ tony@tony:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 1163253
[2] 1163288
WARNING: eatmydata not found: instal it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
(.venv) ◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
Register to rpc_command hook
We use the Python decorators @plugin.hook("rpc_command") to register
to rpc_command hook. The function bellow that decorators is used to
build the payload of the JSON-RPC response we send back to
lightningd each time we receive a rpc_command request. By returning
the dictionary {"result": "continue"}, the plugin tells lightningd "I
don't care about that request do what you were supposed to do":
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
return {"result": "continue"}
plugin.run()
We jump back in our terminal and start our pay-up-to.py plugin like
this:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/chanbackup",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bcli",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/commando",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/funder",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/topology",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/keysend",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/offers",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/pay",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/txprepare",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/spenderp",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/sql",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bookkeeper",
"active": true,
"dynamic": false
},
{
"name": "/home/tony/clnlive/pay-up-to.py",
"active": true,
"dynamic": true
}
]
}
We use ps to check that our plugin is running:
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pay-up
1163640 pts/0 S 0:00 python /home/tony/clnlive/pay-up-to.py
1163658 pts/0 S+ 0:00 rg pay-up
And finally, we call the command getinfo and check that everything is
working correctly:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
While this is working we can't really see that the getinfo request has
been forwarded to pay-up-to.py plugin.
Make the commands hang 2 seconds
Let's make the commands hang 2 seconds each time they are used by a client. This will make our example more tangible:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
time.sleep(2)
return {"result": "continue"}
Let's restart our plugin:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
Now when we call any JSON-RPC command we can see that they hang during
2 seconds before returning the expected response. This is the case
for instance for l1-cli getinfo and l1-cli listpeers.
Take over l1 node
So far the plugin let lightningd take care of the answer even when we
made it slow.
Now, we modify rpc_command_func like this
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
return {"return": {"result": {"BOOM": "I took over your node"}}}
so that each time a client sends a JSON-RPC request it will get the following response:
{
"BOOM": "I took over your node"
}
Let's restart our plugin
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
and send the requests getinfo, listpeers and stop:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli stop
{
"BOOM": "I took over your node"
}
The hook rpc_command hook is so powerful, that we've just made our
node useless. We can't even stop it with the stop command.
Let's stop the node l1 by killing its associated process and restart
it with lightningd command:
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg lightningd
1163255 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l1-regtest
1163291 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l2-regtest
1163998 pts/0 S+ 0:00 rg lightningd
(.venv) ◉ tony@tony:~/clnlive:
$ kill -9 1163255
lightning/contrib/startup_regtest.sh: line 85: 1163255 Killed $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
[1]- Exit 137 test -f "/tmp/l$i-$network/lightningd-$network.pid" || $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
(.venv) ◉ tony@tony:~/clnlive:
$ lightningd --lightning-dir=/tmp/l1-regtest --daemon
We can check using ps that both nodes l1 and l2 are running again:
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg lightningd
1163291 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l2-regtest
1164016 ? Ss 0:00 lightningd --lightning-dir=/tmp/l1-regtest --daemon
1164074 pts/0 S+ 0:00 rg lightningd
Take over getinfo
Now we'll see how to just take over the command getinfo and let
lightningd taking care of the other JSON-RPC commands.
This is possible by filtering on the method field of rpc_command
argument in the function rpc_command_func.
How can we know that rpc_command has a method field?
Let's find out.
We modify rpc_command_func in order to print rpc_command value
representation in the file /tmp/pay-up-to and we'll see the presence
of that field:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
with open("/tmp/pay-up-to", "a") as f:
f.write(repr(rpc_command) + "\n\n")
return {"result": "continue"}
Let's restart our plugin
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
and call the commands getinfo and listpeers like this:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
...
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
Now we can check that the request getinfo and listpeers have been
written into the file /tmp/pay-up-to:
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1164167', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'getinfo', 'id': 'cli:getinfo#1164167', 'params': []}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1164226', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'listpeers', 'id': 'cli:listpeers#1164226', 'params': []}
Note: I don't know why we also have notifications requests written in
that file.
Now we take over the getinfo command only:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "getinfo":
return {"return": {"result": {"BOOM": "I took 'getinfo'"}}}
return {"result": "continue"}
Let's check in our terminal that we have implemented the expected behavior. We restart our plugin
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
and call getinfo and listpeers commands:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"BOOM": "I took over 'getinfo'"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
Chat
Can plugins be written in any language?
Have I understood correctly? The plugin is controling the result of getinfo but nothing else in the node.
Hooks and getmanifest
Before we continue writing our pay-up-to.py plugin, let's take a look
at the getmanifest request/response between lightningd and the plugin
when we start it.
When we start pay-up-to.py plugin, we receive (the plugin) a
getmanifest request like this one:
{
"jsonrpc": "2.0",
"id": 182,
"method": "getmanifest",
"params": {
"allow-deprecated-apis": false
}
}
In order to register to rpc_command hook we add {"name":
"rpc_command"} to the array hooks in the field result of the response
to the getmanifest. So, our response to the getmanifest request looks
like this:
{
"jsonrpc": "2.0",
"id": 182,
"result": {
"dynamic": True,
"options": [],
"rpcmethods": [],
"hooks": [{ "name": "rpc_command" }]
}
}
In the case of the plugin pay-up-to.py, as we are using pyln-client
package, pyln-client does it for us.
Open a channel between the node l1 and l2
As we are going to "modify" the command pay and try it to pay bolt11
invoices, we need a channel open between the node l1 and l2. We do
this using the commands connect and fund_nodes provided by the script
contrib/startup_regtest.sh from the lightning repository:
(.venv) ◉ tony@tony:~/clnlive:
$ connect 1 2
{
"id": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"features": "08a000080269a2",
"direction": "out",
"address": {
"type": "ipv4",
"address": "127.0.0.1",
"port": 7272
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ fund_nodes
Mining into address bcrt1qlz7phgee89vg23fs54an94phxd9au5az9vn6rn... done.
bitcoind balance: 50.00000000
Waiting for lightning node funds... found.
Funding channel from node 1 to node 2. Waiting for confirmation... done.
Params type - array or object
We've seen before that the rpc_command argument in the function
rpc_command_func corresponds to the JSON-RPC request that lightningd
forwards to us. The params field of that request contained the
parameters of the command and can be an ordered array or a key/value
pairs object.
Let's observe that in the case of the pay command.
To do so we modify pay-up-to.py like this
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
with open("/tmp/pay-up-to", "a") as f:
f.write(repr(rpc_command) + "\n\n")
return {"result": "continue"}
plugin.run()
such that every JSON-RPC requests sent to lightningd will be written in
the file /tmp/pay-up-to.
Back to our terminal, we restart our plugin:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
Let's call the pay command with ordered arguments (fake arguments):
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11inv
{
"code": -32602,
"message": "Invalid bolt11: Bad bech32 string"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11inv amountofinv
{
"code": -32602,
"message": "amount_msat|msatoshi: should be a millisatoshi amount: invalid token '\"amountofinv\"'"
}
We don't care about the errors, what we want to is the type of params
field in the pay request. We can check, by looking at the file
/tmp/pay-up-to, that in the case of ordered arguments, params is an
array:
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165357', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165357', 'params': ['bolt11inv']}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165409', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165409', 'params': ['bolt11inv', 'amountofinv']}
Now if we use -k flag and pass the argument by key/value pairs like this
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k pay bolt11=bolt11inv
{
"code": -32602,
"message": "Invalid bolt11: Bad bech32 string"
}
we see that params field in the JSON-RPC request is now a key/value
pairs object:
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165478', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165478', 'params': {'bolt11': 'bolt11inv'}}
In the plugin pay-up-to.py, we'll treat only the case where params is
an array with only one element: the bolt11 invoice.
Take over pay
Let's keep implementing our plugin.
As we want to modify the pay command, let's start by writing the logic
that make the plugin takes over the pay command:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "pay":
return {"return": {"result": {"BOOM": "I took 'pay'"}}}
return {"result": "continue"}
We restart our plugin and check that those changes are effective:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
...
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11
{
"BOOM": "I took over 'pay'"
}
bolt11 argument in pay command
In the plugin pay-up-to.py, we treat only the case where params is
an array with only one element: the bolt11 invoice.
That means we don't treat the case where the bolt11 invoice has no
amount specified. If we treat that case we would have to check for a
second argument amount passed to the pay command.
Let's check if we can retrieve the bolt11 invoice:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "pay":
bolt11 = request["params"][0]
return {"return": {"result": {"BOOM": bolt11}}}
return {"result": "continue"}
We restart our plugin and check that those changes are effective:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11
{
"BOOM": "bolt11"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay boltfoo
{
"BOOM": "boltfoo"
}
Human readable part of bolt11
I reproduce here part of bolt11 specification (Human-Readable Part, Creative Commons Attribution 4.0 International License)
The format for a Lightning invoice uses bech32 encoding.
[...]
The human-readable part of a Lightning invoice consists of two sections:
prefix:ln+ BIP-0173 currency prefix (e.g.lnbcfor Bitcoin mainnet,lntbfor Bitcoin testnet,lntbsfor Bitcoin signet, andlnbcrtfor Bitcoin regtest)amount: optional number in that currency, followed by an optionalmultiplierletter. The unit encoded here is the 'social' convention of a payment unit -- in the case of Bitcoin the unit is 'bitcoin' NOT satoshis.
The following multiplier letters are defined:
m(milli): multiply by 0.001u(micro): multiply by 0.000001n(nano): multiply by 0.000000001p(pico): multiply by 0.000000000001
Examples
Please make a donation of any amount using rhash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad
lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784wPlease send $3 for a cup of nonsense (ナンセンス 1杯) to the same peer, within 1 minute
lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnyNow send $24 for an entire list of things (hashed)
lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7
Limit pay command to 0.001btc
Now we use the function amount that takes a bolt11 invoice with a
mandatory amount and a mandatory multiplier (This is mandatory in our
function but optional in bolt11 specification) to limit the amount we
can pay to a bolt11 invoice.
We "hard code" the limit to 0.001btc and our plugin is now:
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
def amount(bolt11):
multiplier = {
"m": 0.001,
"u": 0.000001,
"n": 0.000000001,
"p": 0.000000000001
}
match = re.match(r"ln(?:bcrt|bc|tbs|tb)([0-9]+)(.)", bolt11)
amount = match[1]
mltp = match[2]
return float(amount) * multiplier[mltp]
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "pay":
bolt11 = request["params"][0]
if amount(bolt11) > 0.001:
return {"return":
{"result":
{
"invoice_too_high": "TODO",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}}}
return {"result": "continue"}
plugin.run()
Note that the fields in the dictionary returned are all hard coded. We'll change this in a moment.
Back to our terminal, we restart pay-up-to.py plugin:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
The node l2 creates a bolt11 invoice with an amount below the
threshold 0.001btc
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.0001btc inv-1 "foo"
{
"payment_hash": "540063fe219eff8419a957a5db7af210b2cb170927b8dacbbff79731e5aa2236",
"expires_at": 1686840846,
"bolt11": "lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq",
"payment_secret": "9a13873900982a3a80013cc9451d23cf921f63c608efb1bb21f9802f16e2ab89",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
and so the node l1 can pay that invoice:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq | jq
{
"destination": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"payment_hash": "540063fe219eff8419a957a5db7af210b2cb170927b8dacbbff79731e5aa2236",
"created_at": 1686236056.629,
"parts": 1,
"amount_msat": 10000000,
"amount_sent_msat": 10000000,
"payment_preimage": "46947a15026a544ceff6b528c4983bdf5d19534f2ac0e37057c527e803e723ac",
"status": "complete"
}
Now the node l2 creates a bolt11 invoice with an amount of 0.002btc
superior to the threshold 0.001btc
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-2 "foo"
{
"payment_hash": "a50b36cf359e47bb08487f6dd759a2c38b076de4aff4c67ca1bab450e85d15f4",
"expires_at": 1686840875,
"bolt11": "lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf",
"payment_secret": "a3d6461c7f53edac1fac6eb0a78bdca50da3b364c742d8b228eb3dcd65605472",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
such that the node l1 can't pay that invoice as we can see below:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf | jq
{
"invoice_too_high": "TODO",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
Let's modify rpc_command_func such that the field invoice_too_high in
the payload response sent back to lightningd contains the amount of
the bolt11 invoice which is superior to the threshold 0.001btc:
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
if request["method"] == "pay":
bolt11 = request["params"][0]
if amount(bolt11) > 0.001:
return {"return":
{"result":
{
"invoice_too_high": str(amount(bolt11)) + "btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}}}
return {"result": "continue"}
Now we can restart our plugin, let the node l2 generates an invoice
with an amount of 0.002btc (too high) and check that the plugin
pay-up-to.py stops the payment and returns the too high amount of the
invoice:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-3 "foo"
{
"payment_hash": "a861020907fdbeae59397aceb098a1e83bfe95e14a75fa36dd7aadbec908bd75",
"expires_at": 1686840947,
"bolt11": "lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6",
"payment_secret": "789272c7650f96b7ca74e7ec84465ccaf36bf7099ccf30e174607a68a489a092",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6 | jq
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
Chat
It seems easy to add plugins!
As I can check visually the amount why would I need a plugin like pay-up-to.py?
You can add custom metrics about the node activity.
(not in the video) Limit pay command to 'limit' which can be defined at plugin startup time
The last thing we can do to make our plugin more capable is to add a
startup option, let say limit, that is used to set the threshold of
the amount we can pay to an invoice. So far it was an hard coded
value equal to 0.001btc.
With pyln-client package, this can be done
using
add_optionmethod of thePluginclass to add a new startup option andusing
get_optionmethod of thePluginclass to get the value of some options set after theinitrequest/response communication betweenlightningdand the plugin when the plugin is started.
So the plugin pay-up-to.py is now implemented like this:
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
def amount(bolt11):
multiplier = {
"m": 0.001,
"u": 0.000001,
"n": 0.000000001,
"p": 0.000000000001
}
match = re.match(r"ln(?:bcrt|bc|tbs|tb)([0-9]+)(.)", bolt11)
amount = match[1]
mltp = match[2]
return float(amount) * multiplier[mltp]
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
limit = float(plugin.get_option("limit").strip("btc"))
if request["method"] == "pay":
bolt11 = request["params"][0]
if amount(bolt11) > limit:
return {"return":
{"result":
{
"invoice_too_high": str(amount(bolt11)) + "btc",
"maximum_is": str(limit) + "btc",
"bolt11": bolt11
}}}
return {"result": "continue"}
plugin.add_option(name="limit",
default="0.001btc",
description="pay bolt11 invoice up to 'limit'")
plugin.run()
Note that we also modified bolt11 value in the payload returned by
rpc_command_func function.
Back to our terminal, we can start pay-up-to.py plugin with limit
startup option set to 0.001btc:
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/pay-up-to.py limit=0.001btc
{
"command": "start",
"plugins": [...]
}
And we can check that the plugin works as expected:
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-4 "foo"
{
"payment_hash": "9c72b37ea796bf24420bb35084aedaeec78b2f6d17e205ddab8341b7b745b9bf",
"expires_at": 1686842009,
"bolt11": "lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex",
"payment_secret": "c5ccc10b077525af28894e0c4b8dd8015857fed14e7fc332f198611f7e660e07",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.0001btc inv-5 "foo"
{
"payment_hash": "6b107224561a7c5841e80d91721eed2480ff5431db6db3c887861ec058c2efb2",
"expires_at": 1686842048,
"bolt11": "lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk",
"payment_secret": "c491bd72e651fe04ed5f9891665b4f40c901fbf4c47981d6e9e82d89b7a40cf2",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk
{
"destination": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"payment_hash": "6b107224561a7c5841e80d91721eed2480ff5431db6db3c887861ec058c2efb2",
"created_at": 1686237261.677,
"parts": 1,
"amount_msat": 10000000,
"amount_sent_msat": 10000000,
"payment_preimage": "200fd3cf48f433dfddc32a3e16d9fd022fde5c53aaa4dad94cc3e9767ff2d545",
"status": "complete"
}
We are done!
Terminal session
We ran the following commands in this order:
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install pyln-client
$ source lightning/contrib/startup_regtest.sh
$ start_ln
$ alias l1-cli
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ ps -ax | rg pay-up
$ l1-cli getinfo
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli listpeers
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli listpeers
$ l1-cli stop
$ ps -ax | rg lightningd
$ kill -9 1163255
$ lightningd --lightning-dir=/tmp/l1-regtest --daemon
$ ps -ax | rg lightningd
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli listpeers
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli listpeers
$ connect 1 2
$ fund_nodes
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli pay bolt11inv
$ l1-cli pay bolt11inv amountofinv
$ l1-cli -k pay bolt11=bolt11inv
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli getinfo
$ l1-cli pay bolt11
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l1-cli pay bolt11
$ l1-cli pay boltfoo
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l2-cli invoice 0.0001btc inv-1 "foo"
$ l1-cli pay lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq | jq
$ l2-cli invoice 0.002btc inv-2 "foo"
$ l1-cli pay lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf | jq
$ l1-cli plugin start $(pwd)/pay-up-to.py
$ l2-cli invoice 0.002btc inv-3 "foo"
$ l1-cli pay lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6 | jq
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/pay-up-to.py limit=0.001btc
$ l2-cli invoice 0.002btc inv-4 "foo"
$ l1-cli pay lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex
$ l2-cli invoice 0.0001btc inv-5 "foo"
$ l1-cli pay lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk
And below you can read the terminal session (command lines and outputs):
◉ tony@tony:~/clnlive:
$ python -m venv .venv
◉ tony@tony:~/clnlive:
$ source .venv/bin/activate
(.venv) ◉ tony@tony:~/clnlive:
$ pip install pyln-client
...
(.venv) ◉ tony@tony:~/clnlive:
$ source lightning/contrib/startup_regtest.sh
lightning-cli is /usr/local/bin/lightning-cli
lightningd is /usr/local/bin/lightningd
Useful commands:
start_ln 3: start three nodes, l1, l2, l3
connect 1 2: connect l1 and l2
fund_nodes: connect all nodes with channels, in a row
stop_ln: shutdown
destroy_ln: remove ln directories
(.venv) ◉ tony@tony:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
[1] 1163253
[2] 1163288
WARNING: eatmydata not found: instal it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
(.venv) ◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [
{
"name": "/usr/local/libexec/c-lightning/plugins/autoclean",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/chanbackup",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bcli",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/commando",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/funder",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/topology",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/keysend",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/offers",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/pay",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/txprepare",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/spenderp",
"active": true,
"dynamic": false
},
{
"name": "/usr/local/libexec/c-lightning/plugins/sql",
"active": true,
"dynamic": true
},
{
"name": "/usr/local/libexec/c-lightning/plugins/bookkeeper",
"active": true,
"dynamic": false
},
{
"name": "/home/tony/clnlive/pay-up-to.py",
"active": true,
"dynamic": true
}
]
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg pay-up
1163640 pts/0 S 0:00 python /home/tony/clnlive/pay-up-to.py
1163658 pts/0 S+ 0:00 rg pay-up
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli stop
{
"BOOM": "I took over your node"
}
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg lightningd
1163255 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l1-regtest
1163291 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l2-regtest
1163998 pts/0 S+ 0:00 rg lightningd
(.venv) ◉ tony@tony:~/clnlive:
$ kill -9 1163255
lightning/contrib/startup_regtest.sh: line 85: 1163255 Killed $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
[1]- Exit 137 test -f "/tmp/l$i-$network/lightningd-$network.pid" || $EATMYDATA "$LIGHTNINGD" "--lightning-dir=/tmp/l$i-$network"
(.venv) ◉ tony@tony:~/clnlive:
$ lightningd --lightning-dir=/tmp/l1-regtest --daemon
(.venv) ◉ tony@tony:~/clnlive:
$ ps -ax | rg lightningd
1163291 pts/0 S 0:00 lightningd --lightning-dir=/tmp/l2-regtest
1164016 ? Ss 0:00 lightningd --lightning-dir=/tmp/l1-regtest --daemon
1164074 pts/0 S+ 0:00 rg lightningd
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"BOOM": "I took over 'getinfo'"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli listpeers
{
"peers": []
}
(.venv) ◉ tony@tony:~/clnlive:
$ connect 1 2
{
"id": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"features": "08a000080269a2",
"direction": "out",
"address": {
"type": "ipv4",
"address": "127.0.0.1",
"port": 7272
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ fund_nodes
Mining into address bcrt1qlz7phgee89vg23fs54an94phxd9au5az9vn6rn... done.
bitcoind balance: 50.00000000
Waiting for lightning node funds... found.
Funding channel from node 1 to node 2. Waiting for confirmation... done.
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11inv
{
"code": -32602,
"message": "Invalid bolt11: Bad bech32 string"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11inv amountofinv
{
"code": -32602,
"message": "amount_msat|msatoshi: should be a millisatoshi amount: invalid token '\"amountofinv\"'"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k pay bolt11=bolt11inv
{
"code": -32602,
"message": "Invalid bolt11: Bad bech32 string"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "03776915f5da2d5801e08639f9da607d51926ae705da1348c06c6ba2a3bfb159bc",
"alias": "BIZARRESPAWN",
"color": "037769",
"num_peers": 1,
"num_pending_channels": 0,
"num_active_channels": 1,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.02.2",
"blockheight": 108,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a000080269a2",
"node": "88a000080269a2",
"channel": "",
"invoice": "02000000024100"
}
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11
{
"BOOM": "I took over 'pay'"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay bolt11
{
"BOOM": "bolt11"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay boltfoo
{
"BOOM": "boltfoo"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.0001btc inv-1 "foo"
{
"payment_hash": "540063fe219eff8419a957a5db7af210b2cb170927b8dacbbff79731e5aa2236",
"expires_at": 1686840846,
"bolt11": "lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq",
"payment_secret": "9a13873900982a3a80013cc9451d23cf921f63c608efb1bb21f9802f16e2ab89",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100u1pjgr6uwsp5ngfcwwgqnq4r4qqp8ny528fre7fp7c7xprhmrweplxqz79hz4wyspp52sqx8l3pnmlcgxdf27jak7hjzzevk9cfy7ud4jal77tnred2ygmqdq9vehk7xqyjw5qcqp29qyysgqjc8cel3q7ktv9kghjykkflcnyfq6mz94eeeycjaptd05hlsq37dznak8yegs624ppvk4ehhlj6lvvtrm24lpatc64u3zte0wgd57geqptq52sq | jq
{
"destination": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"payment_hash": "540063fe219eff8419a957a5db7af210b2cb170927b8dacbbff79731e5aa2236",
"created_at": 1686236056.629,
"parts": 1,
"amount_msat": 10000000,
"amount_sent_msat": 10000000,
"payment_preimage": "46947a15026a544ceff6b528c4983bdf5d19534f2ac0e37057c527e803e723ac",
"status": "complete"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-2 "foo"
{
"payment_hash": "a50b36cf359e47bb08487f6dd759a2c38b076de4aff4c67ca1bab450e85d15f4",
"expires_at": 1686840875,
"bolt11": "lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf",
"payment_secret": "a3d6461c7f53edac1fac6eb0a78bdca50da3b364c742d8b228eb3dcd65605472",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgr6atsp550tyv8rl20k6c8avd6c20z7u55x68vmycapd3v3gav7u6etq23eqpp5559ndne4nermkzzg0akawkdzcw9swm0y4l6vvl9ph269p6zazh6qdq9vehk7xqyjw5qcqp29qyysgq2zrjwxtyzzkxhw3727ccucxm8fmn3tcakf3qsdgze5ghru6h8eghwkp39lp0ve2jcds3lt74w3ktfvghkzuurppkjs6apl529t5wgtcppexjuf | jq
{
"invoice_too_high": "TODO",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli plugin start $(pwd)/pay-up-to.py
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-3 "foo"
{
"payment_hash": "a861020907fdbeae59397aceb098a1e83bfe95e14a75fa36dd7aadbec908bd75",
"expires_at": 1686840947,
"bolt11": "lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6",
"payment_secret": "789272c7650f96b7ca74e7ec84465ccaf36bf7099ccf30e174607a68a489a092",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgr6lnsp50zf893m9p7tt0jn5ulkgg3juetekhacfnn8npct5vpax3fyf5zfqpp54pssyzg8lkl2ukfe0t8tpx9paqala90pff6l5dka02kmajggh46sdq9vehk7xqyjw5qcqp29qyysgqahxaz5qwvqpekm5wf8ar53zrpjpplu7j9s2jwp300wug9tlu2t2hqnu3ecu34pff4h4yhtswj99w5z9cxj9hajm53pm4a0v3f60yr0cp7dq4x6 | jq
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgrk8...pm86hgrqq8rf6dh"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli -k plugin subcommand=start plugin=$(pwd)/pay-up-to.py limit=0.001btc
{
"command": "start",
"plugins": [...]
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.002btc inv-4 "foo"
{
"payment_hash": "9c72b37ea796bf24420bb35084aedaeec78b2f6d17e205ddab8341b7b745b9bf",
"expires_at": 1686842009,
"bolt11": "lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex",
"payment_secret": "c5ccc10b077525af28894e0c4b8dd8015857fed14e7fc332f198611f7e660e07",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex
{
"invoice_too_high": "0.002btc",
"maximum_is": "0.001btc",
"bolt11": "lnbcrt2m1pjgruqesp5chxvzzc8w5j672yffcxyhrwcq9v90lk3feluxvh3nps37lnxpcrspp5n3etxl48j6ljgsstkdggftk6amrcktmdzl3qthdtsdqm0d69hxlsdq9vehk7xqyjw5qcqp29qyysgqfvy5pxmwehujtpdmw7w8k6qq0q4rnt3wsuty4600eeecz99fje23a64uyvvywtalukq8yktn7rv6m5swfdh3wkj98dykggsclxv3glqps0meex"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l2-cli invoice 0.0001btc inv-5 "foo"
{
"payment_hash": "6b107224561a7c5841e80d91721eed2480ff5431db6db3c887861ec058c2efb2",
"expires_at": 1686842048,
"bolt11": "lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk",
"payment_secret": "c491bd72e651fe04ed5f9891665b4f40c901fbf4c47981d6e9e82d89b7a40cf2",
"warning_deadends": "Insufficient incoming capacity, once dead-end peers were excluded"
}
(.venv) ◉ tony@tony:~/clnlive:
$ l1-cli pay lnbcrt100u1pjgruzqsp5cjgm6uhx28lqfm2lnzgkvk60grysr7l5c3ucr4hfaqkcndaypneqpp5dvg8yfzkrf79ss0gpkghy8hdyjq074p3mdkm8jy8sc0vqkxza7eqdq9vehk7xqyjw5qcqp29qyysgqqutvp6e7554xca6wjtqsnec4y4ayqd77cn6penu8ds4g6xp2q474t764ctk2s5prsdzr9mx8h74jc4mj27cnqg426xj5ym2a83qhh4qpnmcuvk
{
"destination": "03f8002413ce9a9aa7e75eb4eae40886ba27acc9448dfb8662cd1e92736f184f28",
"payment_hash": "6b107224561a7c5841e80d91721eed2480ff5431db6db3c887861ec058c2efb2",
"created_at": 1686237261.677,
"parts": 1,
"amount_msat": 10000000,
"amount_sent_msat": 10000000,
"payment_preimage": "200fd3cf48f433dfddc32a3e16d9fd022fde5c53aaa4dad94cc3e9767ff2d545",
"status": "complete"
}
Source code
pay-up-to.py
#!/usr/bin/env python
from pyln.client import Plugin
import json, re, time
plugin = Plugin()
def amount(bolt11):
multiplier = {
"m": 0.001,
"u": 0.000001,
"n": 0.000000001,
"p": 0.000000000001
}
match = re.match(r"ln(?:bcrt|bc|tbs|tb)([0-9]+)(.)", bolt11)
amount = match[1]
mltp = match[2]
return float(amount) * multiplier[mltp]
@plugin.hook("rpc_command")
def rpc_command_func(plugin, rpc_command, **kwargs):
request = rpc_command
limit = float(plugin.get_option("limit").strip("btc"))
if request["method"] == "pay":
bolt11 = request["params"][0]
if amount(bolt11) > limit:
return {"return":
{"result":
{
"invoice_too_high": str(amount(bolt11)) + "btc",
"maximum_is": str(limit) + "btc",
"bolt11": bolt11
}}}
return {"result": "continue"}
plugin.add_option(name="limit",
default="0.001btc",
description="pay bolt11 invoice up to 'limit'")
plugin.run()
pay-up-to
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1164167', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'getinfo', 'id': 'cli:getinfo#1164167', 'params': []}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1164226', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'listpeers', 'id': 'cli:listpeers#1164226', 'params': []}
...
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165357', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165357', 'params': ['bolt11inv']}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165409', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165409', 'params': ['bolt11inv', 'amountofinv']}
{'jsonrpc': '2.0', 'method': 'notifications', 'id': 'cli:notifications#1165478', 'params': {'enable': True}}
{'jsonrpc': '2.0', 'method': 'pay', 'id': 'cli:pay#1165478', 'params': {'bolt11': 'bolt11inv'}}
...