Skip to content

Commit aa65649

Browse files
committed
rpc: add graph import-json to recreate a graph from lnd description
1 parent fd78e5d commit aa65649

File tree

7 files changed

+23012
-4
lines changed

7 files changed

+23012
-4
lines changed

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/cli/graph.py

+13
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ def create(number: int, outfile: Path, version: str, bitcoin_conf: Path, random:
3636
)
3737

3838

39+
@graph.command()
40+
@click.argument("infile", type=click.Path())
41+
@click.option("--outfile", type=click.Path())
42+
def import_json(infile: Path, outfile: Path):
43+
"""
44+
Create a cycle graph with nodes imported from lnd `describegraph` JSON file,
45+
and additionally include 7 extra random outbounds per node. Include lightning
46+
channels and their policies as well.
47+
Returns XML file as string with or without --outfile option.
48+
"""
49+
print(rpc_call("graph_import", {"infile": str(infile), "outfile": str(outfile) if outfile else ""}))
50+
51+
3952
@graph.command()
4053
@click.argument("graph", type=click.Path())
4154
def validate(graph: Path):

Diff for: src/warnet/server.py

+70
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from flask_jsonrpc.app import JSONRPC
2727
from flask_jsonrpc.exceptions import ServerError
2828
from warnet.utils import (
29+
DEFAULT_TAG,
2930
create_cycle_graph,
3031
gen_config_dir,
3132
validate_graph_schema,
@@ -170,6 +171,7 @@ def setup_rpc(self):
170171
self.jsonrpc.register(self.network_export)
171172
# Graph
172173
self.jsonrpc.register(self.graph_generate)
174+
self.jsonrpc.register(self.graph_import)
173175
self.jsonrpc.register(self.graph_validate)
174176
# Debug
175177
self.jsonrpc.register(self.generate_deployment)
@@ -529,6 +531,74 @@ def graph_generate(
529531
self.logger.error(msg)
530532
raise ServerError(message=msg) from e
531533

534+
def graph_import(
535+
self,
536+
infile: str,
537+
outfile: str
538+
) -> str:
539+
with open(infile) as f:
540+
json_graph = json.loads(f.read())
541+
542+
# Start with a connected L1 graph with the right amount of tanks
543+
graph = create_cycle_graph(len(json_graph["nodes"]), version=DEFAULT_TAG, bitcoin_conf=None, random_version=False)
544+
545+
# Initialize all the tanks with basic LN node configurations
546+
for index, n in enumerate(graph.nodes()):
547+
graph.nodes[n]["bitcoin_config"] = f"-uacomment=tank{index:06}"
548+
graph.nodes[n]["ln"] = "lnd"
549+
graph.nodes[n]["ln_cb_image"] = "pinheadmz/circuitbreaker:278737d"
550+
graph.nodes[n]["ln_config"] = "--protocol.wumbo-channels"
551+
552+
# Save a map of LN pubkey -> Tank index
553+
ln_ids = {}
554+
for index, node in enumerate(json_graph["nodes"]):
555+
ln_ids[node["id"]] = index
556+
557+
# Offset for edge IDs
558+
# Note create_cycle_graph() creates L1 edges all with the same id "0"
559+
L1_edges = len(graph.edges)
560+
561+
# Insert LN channels
562+
# Ensure channels are in order by channel ID like lnd describegraph output
563+
sorted_edges = sorted(json_graph["edges"], key=lambda chan: int(chan['channel_id']))
564+
for ln_index, channel in enumerate(sorted_edges):
565+
src = ln_ids[channel["node1_pub"]]
566+
tgt = ln_ids[channel["node2_pub"]]
567+
cap = int(channel["capacity"])
568+
push = cap // 2
569+
openp = f"--local_amt={cap} --push_amt={push}"
570+
srcp = ""
571+
tgtp = ""
572+
if channel["node1_policy"]:
573+
srcp += f" --base_fee_msat={channel['node1_policy']['fee_base_msat']}"
574+
srcp += f" --fee_rate_ppm={channel['node1_policy']['fee_rate_milli_msat']}"
575+
srcp += f" --time_lock_delta={channel['node1_policy']['time_lock_delta']}"
576+
srcp += f" --min_htlc_msat={channel['node1_policy']['min_htlc']}"
577+
srcp += f" --max_htlc_msat={push * 1000}"
578+
if channel["node2_policy"]:
579+
tgtp += f" --base_fee_msat={channel['node2_policy']['fee_base_msat']}"
580+
tgtp += f" --fee_rate_ppm={channel['node2_policy']['fee_rate_milli_msat']}"
581+
tgtp += f" --time_lock_delta={channel['node2_policy']['time_lock_delta']}"
582+
tgtp += f" --min_htlc_msat={channel['node2_policy']['min_htlc']}"
583+
tgtp += f" --max_htlc_msat={push * 1000}"
584+
585+
graph.add_edge(
586+
src,
587+
tgt,
588+
key = ln_index+L1_edges,
589+
channel_open = openp,
590+
source_policy = srcp,
591+
target_policy = tgtp)
592+
593+
if outfile:
594+
file_path = Path(outfile)
595+
nx.write_graphml(graph, file_path, named_key_ids=True)
596+
return f"Generated graph written to file: {outfile}"
597+
bio = BytesIO()
598+
nx.write_graphml(graph, bio, named_key_ids=True)
599+
xml_data = bio.getvalue()
600+
return xml_data.decode("utf-8")
601+
532602
def graph_validate(self, graph_path: str) -> str:
533603
with open(graph_path) as f:
534604
graph = nx.parse_graphml(f.read(), node_type=int, force_multigraph=True)

Diff for: src/warnet/utils.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -389,8 +389,9 @@ def set_execute_permission(file_path):
389389

390390
def create_cycle_graph(n: int, version: str, bitcoin_conf: str | None, random_version: bool):
391391
try:
392-
# Use nx.DiGraph() as base otherwise edges not always made in specific directions
393-
graph = nx.generators.cycle_graph(n, nx.DiGraph())
392+
# Use nx.MultiDiGraph() so we get directed edges (source->target)
393+
# and still allow parallel edges (L1 p2p connections + LN channels)
394+
graph = nx.generators.cycle_graph(n, nx.MultiDiGraph())
394395
except TypeError as e:
395396
msg = f"Failed to create graph: {e}"
396397
logger.error(msg)

0 commit comments

Comments
 (0)