Skip to content

Commit 3ed2a1e

Browse files
authored
Merge pull request #315 from pinheadmz/ln-ch-policies
support channel policies in LN edges
2 parents e285997 + eea3b85 commit 3ed2a1e

File tree

18 files changed

+23322
-225
lines changed

18 files changed

+23322
-225
lines changed

Diff for: 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 |

Diff for: 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

Diff for: docs/warcli.md

+12
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ options:
105105
| bitcoin_conf | Path | | |
106106
| random | Bool | | False |
107107

108+
### `warcli graph import-json`
109+
Create a cycle graph with nodes imported from lnd `describegraph` JSON file,
110+
and additionally include 7 extra random outbounds per node. Include lightning
111+
channels and their policies as well.
112+
Returns XML file as string with or without --outfile option.
113+
114+
options:
115+
| name | type | required | default |
116+
|---------|--------|------------|-----------|
117+
| infile | Path | yes | |
118+
| outfile | Path | | |
119+
108120
### `warcli graph validate`
109121
Validate a \<graph file> against the schema.
110122

Diff for: src/backends/compose/compose_backend.py

+7-39
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from warnet.status import RunningStatus
1717
from warnet.tank import Tank
1818
from warnet.utils import (
19-
default_bitcoin_conf_args,
2019
get_architecture,
2120
parse_raw_messages,
2221
)
@@ -340,24 +339,6 @@ def generate_deployment_file(self, warnet):
340339
dirs_exist_ok=True,
341340
)
342341

343-
def config_args(self, tank: Tank):
344-
args = self.default_config_args(tank)
345-
if tank.bitcoin_config is not None:
346-
args = f"{args} -{tank.bitcoin_config.replace(',', ' -')}"
347-
return args
348-
349-
def default_config_args(self, tank):
350-
defaults = default_bitcoin_conf_args()
351-
defaults += f" -rpcuser={tank.rpc_user}"
352-
defaults += f" -rpcpassword={tank.rpc_password}"
353-
defaults += f" -rpcport={tank.rpc_port}"
354-
defaults += f" -zmqpubrawblock=tcp://0.0.0.0:{tank.zmqblockport}"
355-
defaults += f" -zmqpubrawtx=tcp://0.0.0.0:{tank.zmqtxport}"
356-
# connect to initial peers as defined in graph file
357-
for dst_index in tank.init_peers:
358-
defaults += f" -addnode={self.get_container_name(dst_index, ServiceType.BITCOIN)}"
359-
return defaults
360-
361342
def add_services(self, tank: Tank, compose):
362343
services = compose["services"]
363344
assert tank.index is not None
@@ -385,13 +366,17 @@ def add_services(self, tank: Tank, compose):
385366
# Pre-built regular release
386367
image = f"{DOCKER_REGISTRY}:{tank.version}"
387368
services[container_name]["image"] = image
369+
370+
peers = [self.get_container_name(dst_index, ServiceType.BITCOIN) for dst_index in tank.init_peers]
371+
args = tank.get_bitcoin_conf(peers)
372+
388373
# Add common bitcoind service details
389374
services[container_name].update(
390375
{
391376
"container_name": container_name,
392377
# logging with json-file to support log shipping with promtail into loki
393378
"logging": {"driver": "json-file", "options": {"max-size": "10m"}},
394-
"environment": {"BITCOIN_ARGS": self.config_args(tank)},
379+
"environment": {"BITCOIN_ARGS": args},
395380
"networks": {
396381
tank.network_name: {
397382
"ipv4_address": f"{tank.ipv4}",
@@ -436,28 +421,11 @@ def add_lnd_service(self, tank, compose):
436421
ln_cb_container_name = self.get_container_name(tank.index, ServiceType.CIRCUITBREAKER)
437422
bitcoin_container_name = self.get_container_name(tank.index, ServiceType.BITCOIN)
438423
# These args are appended to the Dockerfile `ENTRYPOINT ["lnd"]`
439-
args = [
440-
"--noseedbackup",
441-
"--norest",
442-
"--debuglevel=debug",
443-
"--accept-keysend",
444-
"--bitcoin.active",
445-
"--bitcoin.regtest",
446-
"--bitcoin.node=bitcoind",
447-
f"--bitcoind.rpcuser={tank.rpc_user}",
448-
f"--bitcoind.rpcpass={tank.rpc_password}",
449-
f"--bitcoind.rpchost={tank.ipv4}:{tank.rpc_port}",
450-
f"--bitcoind.zmqpubrawblock=tcp://{tank.ipv4}:{tank.zmqblockport}",
451-
f"--bitcoind.zmqpubrawtx=tcp://{tank.ipv4}:{tank.zmqtxport}",
452-
f"--externalip={tank.lnnode.ipv4}",
453-
f"--rpclisten=0.0.0.0:{tank.lnnode.rpc_port}",
454-
f"--alias={tank.index}",
455-
f"--tlsextradomain={ln_container_name}",
456-
]
424+
args = tank.lnnode.get_conf(ln_container_name, bitcoin_container_name)
457425
services[ln_container_name] = {
458426
"container_name": ln_container_name,
459427
"image": tank.lnnode.image,
460-
"command": " ".join(args),
428+
"command": args,
461429
"networks": {
462430
tank.network_name: {
463431
"ipv4_address": f"{tank.lnnode.ipv4}",

Diff for: src/backends/kubernetes/kubernetes_backend.py

+5-33
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from kubernetes.stream import stream
1818
from warnet.status import RunningStatus
1919
from warnet.tank import Tank
20-
from warnet.utils import default_bitcoin_conf_args, parse_raw_messages
20+
from warnet.utils import parse_raw_messages
2121

2222
DOCKER_REGISTRY_CORE = "bitcoindevproject/bitcoin"
2323
LOCAL_REGISTRY = "warnet/bitcoin-core"
@@ -313,18 +313,6 @@ def generate_deployment_file(self, warnet):
313313
"""
314314
pass
315315

316-
def default_bitcoind_config_args(self, tank):
317-
defaults = default_bitcoin_conf_args()
318-
defaults += f" -rpcuser={tank.rpc_user}"
319-
defaults += f" -rpcpassword={tank.rpc_password}"
320-
defaults += f" -rpcport={tank.rpc_port}"
321-
defaults += f" -zmqpubrawblock=tcp://0.0.0.0:{tank.zmqblockport}"
322-
defaults += f" -zmqpubrawtx=tcp://0.0.0.0:{tank.zmqtxport}"
323-
# connect to initial peers as defined in graph file
324-
for dst_index in tank.init_peers:
325-
defaults += f" -addnode={self.get_service_name(dst_index)}"
326-
return defaults
327-
328316
def create_bitcoind_container(self, tank: Tank) -> client.V1Container:
329317
self.log.debug(f"Creating bitcoind container for tank {tank.index}")
330318
container_name = BITCOIN_CONTAINER_NAME
@@ -356,8 +344,8 @@ def create_bitcoind_container(self, tank: Tank) -> client.V1Container:
356344
else:
357345
container_image = f"{DOCKER_REGISTRY_CORE}:{tank.version}"
358346

359-
bitcoind_options = self.default_bitcoind_config_args(tank)
360-
bitcoind_options += f" {tank.bitcoin_config}"
347+
peers = [self.get_service_name(dst_index) for dst_index in tank.init_peers]
348+
bitcoind_options = tank.get_bitcoin_conf(peers)
361349
container_env = [client.V1EnvVar(name="BITCOIN_ARGS", value=bitcoind_options)]
362350

363351
bitcoind_container = client.V1Container(
@@ -454,28 +442,12 @@ def create_lnd_container(
454442
# These args are appended to the Dockerfile `ENTRYPOINT ["lnd"]`
455443
bitcoind_rpc_host = f"{bitcoind_service_name}.{self.namespace}"
456444
lightning_dns = f"lightning-{tank.index}.{self.namespace}"
457-
args = [
458-
"--noseedbackup",
459-
"--norest",
460-
"--debuglevel=debug",
461-
"--accept-keysend",
462-
"--bitcoin.active",
463-
"--bitcoin.regtest",
464-
"--bitcoin.node=bitcoind",
465-
f"--bitcoind.rpcuser={tank.rpc_user}",
466-
f"--bitcoind.rpcpass={tank.rpc_password}",
467-
f"--bitcoind.rpchost={bitcoind_rpc_host}:{tank.rpc_port}",
468-
f"--bitcoind.zmqpubrawblock={bitcoind_rpc_host}:{tank.zmqblockport}",
469-
f"--bitcoind.zmqpubrawtx={bitcoind_rpc_host}:{tank.zmqtxport}",
470-
f"--rpclisten=0.0.0.0:{tank.lnnode.rpc_port}",
471-
f"--externalhosts={lightning_dns}",
472-
f"--alias={tank.index}",
473-
]
445+
args = tank.lnnode.get_conf(lightning_dns, bitcoind_rpc_host)
474446
self.log.debug(f"Creating lightning container for tank {tank.index} using {args=:}")
475447
lightning_container = client.V1Container(
476448
name=LN_CONTAINER_NAME,
477449
image=tank.lnnode.image,
478-
args=args,
450+
args=args.split(" "),
479451
env=[
480452
client.V1EnvVar(name="LN_IMPL", value=tank.lnnode.impl),
481453
],

Diff for: src/cli/graph.py

+75-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
from io import BytesIO
23
from pathlib import Path
34

@@ -28,7 +29,79 @@ def create(number: int, outfile: Path, version: str, bitcoin_conf: Path, random:
2829
if outfile:
2930
file_path = Path(outfile)
3031
nx.write_graphml(graph, file_path, named_key_ids=True)
31-
return f"Generated graph written to file: {outfile}"
32+
bio = BytesIO()
33+
nx.write_graphml(graph, bio, named_key_ids=True)
34+
xml_data = bio.getvalue()
35+
print(xml_data.decode("utf-8"))
36+
37+
38+
@graph.command()
39+
@click.argument("infile", type=click.Path())
40+
@click.option("--outfile", type=click.Path())
41+
def import_json(infile: Path, outfile: Path):
42+
"""
43+
Create a cycle graph with nodes imported from lnd `describegraph` JSON file,
44+
and additionally include 7 extra random outbounds per node. Include lightning
45+
channels and their policies as well.
46+
Returns XML file as string with or without --outfile option.
47+
"""
48+
with open(infile) as f:
49+
json_graph = json.loads(f.read())
50+
51+
# Start with a connected L1 graph with the right amount of tanks
52+
graph = create_cycle_graph(len(json_graph["nodes"]), version=DEFAULT_TAG, bitcoin_conf=None, random_version=False)
53+
54+
# Initialize all the tanks with basic LN node configurations
55+
for index, n in enumerate(graph.nodes()):
56+
graph.nodes[n]["bitcoin_config"] = f"-uacomment=tank{index:06}"
57+
graph.nodes[n]["ln"] = "lnd"
58+
graph.nodes[n]["ln_cb_image"] = "pinheadmz/circuitbreaker:278737d"
59+
graph.nodes[n]["ln_config"] = "--protocol.wumbo-channels"
60+
61+
# Save a map of LN pubkey -> Tank index
62+
ln_ids = {}
63+
for index, node in enumerate(json_graph["nodes"]):
64+
ln_ids[node["id"]] = index
65+
66+
# Offset for edge IDs
67+
# Note create_cycle_graph() creates L1 edges all with the same id "0"
68+
L1_edges = len(graph.edges)
69+
70+
# Insert LN channels
71+
# Ensure channels are in order by channel ID like lnd describegraph output
72+
sorted_edges = sorted(json_graph["edges"], key=lambda chan: int(chan['channel_id']))
73+
for ln_index, channel in enumerate(sorted_edges):
74+
src = ln_ids[channel["node1_pub"]]
75+
tgt = ln_ids[channel["node2_pub"]]
76+
cap = int(channel["capacity"])
77+
push = cap // 2
78+
openp = f"--local_amt={cap} --push_amt={push}"
79+
srcp = ""
80+
tgtp = ""
81+
if channel["node1_policy"]:
82+
srcp += f" --base_fee_msat={channel['node1_policy']['fee_base_msat']}"
83+
srcp += f" --fee_rate_ppm={channel['node1_policy']['fee_rate_milli_msat']}"
84+
srcp += f" --time_lock_delta={channel['node1_policy']['time_lock_delta']}"
85+
srcp += f" --min_htlc_msat={channel['node1_policy']['min_htlc']}"
86+
srcp += f" --max_htlc_msat={push * 1000}"
87+
if channel["node2_policy"]:
88+
tgtp += f" --base_fee_msat={channel['node2_policy']['fee_base_msat']}"
89+
tgtp += f" --fee_rate_ppm={channel['node2_policy']['fee_rate_milli_msat']}"
90+
tgtp += f" --time_lock_delta={channel['node2_policy']['time_lock_delta']}"
91+
tgtp += f" --min_htlc_msat={channel['node2_policy']['min_htlc']}"
92+
tgtp += f" --max_htlc_msat={push * 1000}"
93+
94+
graph.add_edge(
95+
src,
96+
tgt,
97+
key = ln_index+L1_edges,
98+
channel_open = openp,
99+
source_policy = srcp,
100+
target_policy = tgtp)
101+
102+
if outfile:
103+
file_path = Path(outfile)
104+
nx.write_graphml(graph, file_path, named_key_ids=True)
32105
bio = BytesIO()
33106
nx.write_graphml(graph, bio, named_key_ids=True)
34107
xml_data = bio.getvalue()
@@ -42,5 +115,5 @@ def validate(graph: Path):
42115
Validate a <graph file> against the schema.
43116
"""
44117
with open(graph) as f:
45-
graph = nx.parse_graphml(f.read(), node_type=int)
118+
graph = nx.parse_graphml(f.read(), node_type=int, force_multigraph=True)
46119
return validate_graph_schema(graph)

0 commit comments

Comments
 (0)