Skip to content

Commit 5b25d7a

Browse files
committed
LN: parse source/target channel policies from graphml
1 parent b780b18 commit 5b25d7a

File tree

7 files changed

+111
-44
lines changed

7 files changed

+111
-44
lines changed

docs/graph.md

+12-6
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,12 @@ lightning network channel (see [lightning.md](lightning.md)).
5252
<key id="collect_logs" attr.name="collect_logs" attr.type="boolean" for="node" />
5353
<key id="build_args" attr.name="build_args" attr.type="string" for="node" />
5454
<key id="ln" attr.name="ln" attr.type="string" for="node" />
55-
<key id="ln-image" attr.name="ln-image" attr.type="string" for="node" />
56-
<key id="ln-cb-image" attr.name="ln-cb-image" attr.type="string" for="node" />
57-
<key id="channel" attr.name="channel" attr.type="number" for="edge" />
55+
<key id="ln_image" attr.name="ln_image" attr.type="string" for="node" />
56+
<key id="ln_cb_image" attr.name="ln_cb_image" attr.type="string" for="node" />
57+
<key id="ln_config" attr.name="ln_config" attr.type="string" for="node" />
58+
<key id="channel_open" attr.name="channel_open" attr.type="string" for="edge" />
59+
<key id="source_policy" attr.name="source_policy" attr.type="string" for="edge" />
60+
<key id="target_policy" attr.name="target_policy" attr.type="string" for="edge" />
5861
<graph edgedefault="directed">
5962
<!-- <nodes> -->
6063
<!-- <edges> -->
@@ -72,6 +75,9 @@ lightning network channel (see [lightning.md](lightning.md)).
7275
| collect_logs | node | boolean | False | Whether to collect Bitcoin Core debug logs with Promtail |
7376
| build_args | node | string | | A string of configure options used when building Bitcoin Core from source code, e.g. '--without-gui --disable-tests' |
7477
| ln | node | string | | Attach a lightning network node of this implementation (currently only supports 'lnd') |
75-
| ln-image | node | string | | Specify a lightning network node image from Dockerhub with the format repository/image:tag |
76-
| ln-cb-image | node | string | | Specify a lnd Circuit Breaker image from Dockerhub with the format repository/image:tag |
77-
| channel | edge | number | | Indicate that this edge is a lightning channel with this specified capacity |
78+
| ln_image | node | string | | Specify a lightning network node image from Dockerhub with the format repository/image:tag |
79+
| ln_cb_image | node | string | | Specify a lnd Circuit Breaker image from Dockerhub with the format repository/image:tag |
80+
| ln_config | node | string | | A string of arguments for the lightning network node in command-line format, e.g. '--protocol.wumbo-channels --bitcoin.timelockdelta=80' |
81+
| channel_open | edge | string | | Indicate that this edge is a lightning channel with these arguments passed to lnd openchannel |
82+
| source_policy | edge | string | | Update the channel originator policy by passing these arguments passed to lnd updatechanpolicy |
83+
| target_policy | edge | string | | Update the channel partner policy by passing these arguments passed to lnd updatechanpolicy |

docs/lightning.md

+14-8
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,24 @@ Example:
1717

1818
## Adding LN channels to graph
1919

20-
LN channels are represented in the graphml file as edges with an extra data element
21-
with key `"channel"` and a value in satoshis representing channel capacity. Note
22-
that this data element is the only difference between LN channels and regular bitcoin
23-
p2p connections. The graph will be considered a `MultiDiGraph` and contain two different
24-
kinds of edges.
20+
LN channels are represented in the graphml file as edges with extra data elements
21+
that correspond to arguments to the lnd `openchannel` and `updatechanpolicy` RPC
22+
commands. The keys are:
23+
24+
- `"channel_open"` (arguments added to `openchannel`)
25+
- `"target_policy"` or `"source_policy"` (arguments added to `updatechanpolicy`)
26+
27+
The key `"channel_open"` is required to open a LN channel in warnet, and to
28+
identify an edge in the graphml file as a LN channel.
2529

2630
Example:
2731

2832
```
29-
<edge id="0" source="0" target="1">
30-
<data key="channel">100000</data>
31-
</edge>
33+
<edge id="5" source="0" target="1">
34+
<data key="channel_open">--local_amt=100000</data>
35+
<data key="source_policy">--base_fee_msat=100 --fee_rate_ppm=5 --time_lock_delta=18</data>
36+
<data key="target_policy">--base_fee_msat=2200 --fee_rate_ppm=13 --time_lock_delta=20</data>
37+
</edge>
3238
```
3339

3440
A complete example graph with LN nodes and channels is included in the test

src/schema/graph_schema.json

+11-5
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@
3030
"ln": {
3131
"type": "string",
3232
"comment": "Attach a lightning network node of this implementation (currently only supports 'lnd')"},
33-
"ln-image": {
33+
"ln_image": {
3434
"type": "string",
3535
"comment": "Specify a lightning network node image from Dockerhub with the format repository/image:tag"},
36-
"ln-cb-image": {
36+
"ln_cb_image": {
3737
"type": "string",
3838
"comment": "Specify a lnd Circuit Breaker image from Dockerhub with the format repository/image:tag"},
3939
"ln_config": {
@@ -50,9 +50,15 @@
5050
"edge": {
5151
"type": "object",
5252
"properties": {
53-
"channel": {
54-
"type": "number",
55-
"comment": "Indicate that this edge is a lightning channel with this specified capacity"}
53+
"channel_open": {
54+
"type": "string",
55+
"comment": "Indicate that this edge is a lightning channel with these arguments passed to lnd openchannel"},
56+
"source_policy": {
57+
"type": "string",
58+
"comment": "Update the channel originator policy by passing these arguments passed to lnd updatechanpolicy"},
59+
"target_policy": {
60+
"type": "string",
61+
"comment": "Update the channel partner policy by passing these arguments passed to lnd updatechanpolicy"}
5662
},
5763
"additionalProperties": false,
5864
"required": []

src/warnet/lnnode.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,33 @@ def getnewaddress(self):
6868

6969
def getURI(self):
7070
res = self.lncli("getinfo")
71+
if len(res["uris"]) < 1:
72+
return None
7173
return res["uris"][0]
7274

7375
def get_wallet_balance(self):
7476
res = self.lncli("walletbalance")
7577
return res
7678

77-
def open_channel_to_tank(self, index, amt):
79+
# returns the channel point in the form txid:output_index
80+
def open_channel_to_tank(self, index: int, policy: str) -> str:
7881
tank = self.warnet.tanks[index]
7982
[pubkey, host] = tank.lnnode.getURI().split("@")
80-
res = self.lncli(f"openchannel --node_key={pubkey} --connect={host} --local_amt={amt}")
81-
return res
83+
txid = self.lncli(f"openchannel --node_key={pubkey} --connect={host} {policy}")["funding_txid"]
84+
# Why doesn't LND return the output index as well?
85+
# Do they charge by the RPC call or something?!
86+
pending = self.lncli("pendingchannels")
87+
for chan in pending["pending_open_channels"]:
88+
if txid in chan["channel"]["channel_point"]:
89+
return chan["channel"]["channel_point"]
90+
raise Exception(f"Opened channel with txid {txid} not found in pending channels")
91+
92+
def update_channel_policy(self, chan_point: str, policy: str) -> str:
93+
ret = self.lncli(f"updatechanpolicy --chan_point={chan_point} {policy}")
94+
if len(ret["failed_updates"]) == 0:
95+
return ret
96+
else:
97+
raise Exception(ret)
8298

8399
def connect_to_tank(self, index):
84100
tank = self.warnet.tanks[index]

src/warnet/warnet.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ def tanks_from_graph(self):
150150
# import edges as list of destinations to connect to
151151
for edge in self.graph.edges(data=True):
152152
(src, dst, data) = edge
153-
if "channel" in data:
153+
# Ignore LN edges for now
154+
if "channel_open" in data:
154155
continue
155156
if src == node_id:
156157
tank.init_peers.append(int(dst))

test/data/ln.graphml

+21-14
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,39 @@
11
<?xml version="1.0" encoding="UTF-8"?><graphml xmlns="http://graphml.graphdrawing.org/xmlns">
2-
<key attr.name="version" attr.type="string" for="node" id="version"/>
3-
<key attr.name="bitcoin_config" attr.type="string" for="node" id="bitcoin_config"/>
4-
<key attr.name="tc_netem" attr.type="string" for="node" id="tc_netem"/>
5-
<key attr.name="ln" attr.type="string" for="node" id="ln"/>
6-
<key attr.name="ln_image" attr.type="string" for="node" id="ln_image"/>
7-
<key attr.name="ln_cb_image" attr.type="string" for="node" id="ln_cb_image"/>
8-
<key attr.name="ln_config" attr.type="string" for="node" id="ln_config"/>
9-
<key attr.name="channel" attr.type="string" for="edge" id="channel"/>
10-
<key attr.name="collect_logs" attr.type="boolean" for="node" id="collect_logs"/>
11-
<key attr.name="image" attr.type="string" for="node" id="image"/>
2+
<key id="version" attr.name="version" attr.type="string" for="node" />
3+
<key id="image" attr.name="image" attr.type="string" for="node" />
4+
<key id="bitcoin_config" attr.name="bitcoin_config" attr.type="string" for="node" />
5+
<key id="tc_netem" attr.name="tc_netem" attr.type="string" for="node" />
6+
<key id="exporter" attr.name="exporter" attr.type="boolean" for="node" />
7+
<key id="collect_logs" attr.name="collect_logs" attr.type="boolean" for="node" />
8+
<key id="build_args" attr.name="build_args" attr.type="string" for="node" />
9+
<key id="ln" attr.name="ln" attr.type="string" for="node" />
10+
<key id="ln_image" attr.name="ln_image" attr.type="string" for="node" />
11+
<key id="ln_cb_image" attr.name="ln_cb_image" attr.type="string" for="node" />
12+
<key id="ln_config" attr.name="ln_config" attr.type="string" for="node" />
13+
<key id="channel_open" attr.name="channel_open" attr.type="string" for="edge" />
14+
<key id="source_policy" attr.name="source_policy" attr.type="string" for="edge" />
15+
<key id="target_policy" attr.name="target_policy" attr.type="string" for="edge" />
1216
<graph edgedefault="directed">
1317
<node id="0">
1418
<data key="version">26.0</data>
1519
<data key="bitcoin_config">-uacomment=w0</data>
1620
<data key="ln">lnd</data>
21+
<data key="ln_image">lightninglabs/lnd:v0.15.5-beta</data>
1722
<data key="collect_logs">true</data>
1823
</node>
1924
<node id="1">
2025
<data key="version">26.0</data>
2126
<data key="bitcoin_config">-uacomment=w1</data>
2227
<data key="ln">lnd</data>
23-
<data key="ln_image">lightninglabs/lnd:v0.15.5-beta</data>
2428
<data key="ln_cb_image">pinheadmz/circuitbreaker:278737d</data>
2529
<data key="collect_logs">true</data>
26-
<data key="ln_config">--bitcoin.timelockdelta=20</data>
2730
</node>
2831
<node id="2">
2932
<data key="version">26.0</data>
3033
<data key="bitcoin_config">-uacomment=w2</data>
3134
<data key="ln">lnd</data>
3235
<data key="ln_cb_image">pinheadmz/circuitbreaker:278737d</data>
36+
<data key="ln_config">--bitcoin.timelockdelta=33</data>
3337
</node>
3438
<node id="3">
3539
<data key="version">26.0</data>
@@ -38,13 +42,16 @@
3842
<edge id="1" source="0" target="1"></edge>
3943
<edge id="2" source="1" target="2"></edge>
4044
<edge id="3" source="2" target="3"></edge>
45+
<edge id="4" source="2" target="0"></edge>
4146
<edge id="4" source="3" target="0"></edge>
4247
<!-- LN channels -->
4348
<edge id="5" source="0" target="1">
44-
<data key="channel">100000</data>
49+
<data key="channel_open">--local_amt=100000</data>
50+
<data key="target_policy">--base_fee_msat=2200 --fee_rate_ppm=13 --time_lock_delta=20</data>
4551
</edge>
4652
<edge id="6" source="1" target="2">
47-
<data key="channel">100000</data>
53+
<data key="channel_open">--local_amt=100000 --push_amt=50000</data>
54+
<data key="source_policy">--base_fee_msat=5500 --fee_rate_ppm=3 --time_lock_delta=40</data>
4855
</edge>
4956
</graph>
5057
</graphml>

test/ln_test.py

+32-7
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,18 @@ def get_cb_forwards(index):
4444
base.warcli("scenarios run ln_init")
4545
base.wait_for_all_scenarios()
4646

47-
print("\nEnsuring channel policy settings")
48-
chans = json.loads(base.warcli("lncli 1 describegraph"))["edges"]
49-
for chan in chans:
50-
# node_1 or node_2 is tank 1 with its non-default --bitcoin.timelockdelta=20
51-
if chan["node1_policy"]["time_lock_delta"] != 20:
52-
assert chan["node2_policy"]["time_lock_delta"] == 20
47+
print("\nEnsuring node-level channel policy settings")
48+
chan_id = json.loads(base.warcli("lncli 2 listchannels"))["channels"][0]["chan_id"]
49+
chan = json.loads(base.warcli(f"lncli 2 getchaninfo {chan_id}"))
50+
# node_1 or node_2 is tank 2 with its non-default --bitcoin.timelockdelta=33
51+
if chan["node1_policy"]["time_lock_delta"] != 33:
52+
assert chan["node2_policy"]["time_lock_delta"] == 33
5353

5454
print("\nEnsuring no circuit breaker forwards yet")
5555
assert len(get_cb_forwards(1)["forwards"]) == 0
5656

5757
print("\nTest LN payment from 0 -> 2")
58-
inv = json.loads(base.warcli("lncli 2 addinvoice --amt=1234"))["payment_request"]
58+
inv = json.loads(base.warcli("lncli 2 addinvoice --amt=2000"))["payment_request"]
5959

6060
print(f"\nGot invoice from node 2: {inv}")
6161
print("\nPaying invoice from node 0...")
@@ -71,7 +71,32 @@ def check_invoices():
7171
return False
7272
base.wait_for_predicate(check_invoices)
7373

74+
print("\nEnsuring channel-level channel policy settings: source")
75+
payment = json.loads(base.warcli("lncli 0 listpayments"))["payments"][0]
76+
assert payment["fee_msat"] == "5506"
77+
7478
print("\nEnsuring circuit breaker tracked payment")
7579
assert len(get_cb_forwards(1)["forwards"]) == 1
7680

81+
print("\nTest LN payment from 2 -> 0")
82+
inv = json.loads(base.warcli("lncli 0 addinvoice --amt=1000"))["payment_request"]
83+
84+
print(f"\nGot invoice from node 0: {inv}")
85+
print("\nPaying invoice from node 2...")
86+
print(base.warcli(f"lncli 2 payinvoice -f {inv}"))
87+
88+
print("Waiting for payment success")
89+
def check_invoices():
90+
invs = json.loads(base.warcli("lncli 0 listinvoices"))["invoices"]
91+
if len(invs) > 0 and invs[0]["state"] == "SETTLED":
92+
print("\nSettled!")
93+
return True
94+
else:
95+
return False
96+
base.wait_for_predicate(check_invoices)
97+
98+
print("\nEnsuring channel-level channel policy settings: target")
99+
payment = json.loads(base.warcli("lncli 2 listpayments"))["payments"][0]
100+
assert payment["fee_msat"] == "2213"
101+
77102
base.stop_server()

0 commit comments

Comments
 (0)