Skip to content

Commit 2a03988

Browse files
authored
Merge pull request bitcoin-dev-project#408 from willcl-ark/refactor-graph-test-cln
[WIP] Refactor graph test cln
2 parents a71d379 + 6febbc7 commit 2a03988

File tree

13 files changed

+766
-248
lines changed

13 files changed

+766
-248
lines changed

src/backend/kubernetes_backend.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,12 @@ def ln_cli(self, tank: Tank, command: list[str]):
265265
self.log.debug(f"Running lncli {cmd=:} on {tank.index=:}")
266266
return self.exec_run(tank.index, ServiceType.LIGHTNING, cmd)
267267

268+
def ln_pub_key(self, tank) -> str:
269+
if tank.lnnode is None:
270+
raise Exception("No LN node configured for tank")
271+
self.log.debug(f"Getting pub key for tank {tank.index}")
272+
return tank.lnnode.get_pub_key()
273+
268274
def get_bitcoin_cli(self, tank: Tank, method: str, params=None):
269275
if params:
270276
cmd = f"bitcoin-cli -regtest -rpcuser={tank.rpc_user} -rpcport={tank.rpc_port} -rpcpassword={tank.rpc_password} {method} {' '.join(map(str, params))}"
@@ -468,14 +474,21 @@ def remove_prometheus_service_monitors(self, tanks):
468474
def get_lnnode_hostname(self, index: int) -> str:
469475
return f"{self.get_service_name(index, ServiceType.LIGHTNING)}.{self.namespace}"
470476

471-
def create_lnd_container(
472-
self, tank, bitcoind_service_name, volume_mounts
473-
) -> client.V1Container:
477+
def create_ln_container(self, tank, bitcoind_service_name, volume_mounts) -> client.V1Container:
474478
# These args are appended to the Dockerfile `ENTRYPOINT ["lnd"]`
475479
bitcoind_rpc_host = f"{bitcoind_service_name}.{self.namespace}"
476480
lightning_dns = self.get_lnnode_hostname(tank.index)
477481
args = tank.lnnode.get_conf(lightning_dns, bitcoind_rpc_host)
478482
self.log.debug(f"Creating lightning container for tank {tank.index} using {args=:}")
483+
lightning_ready_probe = ""
484+
if tank.lnnode.impl == "lnd":
485+
lightning_ready_probe = "lncli --network=regtest getinfo"
486+
elif tank.lnnode.impl == "cln":
487+
lightning_ready_probe = "lightning-cli --network=regtest getinfo"
488+
else:
489+
raise Exception(
490+
f"Lightning node implementation {tank.lnnode.impl} for tank {tank.index} not supported"
491+
)
479492
lightning_container = client.V1Container(
480493
name=LN_CONTAINER_NAME,
481494
image=tank.lnnode.image,
@@ -486,12 +499,10 @@ def create_lnd_container(
486499
readiness_probe=client.V1Probe(
487500
failure_threshold=1,
488501
success_threshold=3,
489-
initial_delay_seconds=1,
502+
initial_delay_seconds=10,
490503
period_seconds=2,
491504
timeout_seconds=2,
492-
_exec=client.V1ExecAction(
493-
command=["/bin/sh", "-c", "lncli --network=regtest getinfo"]
494-
),
505+
_exec=client.V1ExecAction(command=["/bin/sh", "-c", lightning_ready_probe]),
495506
),
496507
security_context=client.V1SecurityContext(
497508
privileged=True,
@@ -681,7 +692,7 @@ def deploy_pods(self, warnet):
681692
raise e
682693
self.client.create_namespaced_service(namespace=self.namespace, body=bitcoind_service)
683694

684-
# Create and deploy LND pod
695+
# Create and deploy a lightning pod
685696
if tank.lnnode:
686697
conts = []
687698
vols = []
@@ -700,9 +711,9 @@ def deploy_pods(self, warnet):
700711
)
701712
# Add circuit breaker container
702713
conts.append(self.create_circuitbreaker_container(tank, volume_mounts))
703-
# Add lnd container
714+
# Add lightning container
704715
conts.append(
705-
self.create_lnd_container(tank, bitcoind_service.metadata.name, volume_mounts)
716+
self.create_ln_container(tank, bitcoind_service.metadata.name, volume_mounts)
706717
)
707718
# Put it all together in a pod
708719
lnd_pod = self.create_pod_object(

src/cli/main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ def lncli(node: int, command: tuple, network: str):
8787
)
8888

8989

90+
@cli.command(context_settings={"ignore_unknown_options": True})
91+
@click.argument("node", type=int)
92+
@click.option("--network", default="warnet", show_default=True, type=str)
93+
def ln_pub_key(node: int, network: str):
94+
"""
95+
Get lightning node pub key on <node> in [network]
96+
"""
97+
print(
98+
rpc_call(
99+
"tank_ln_pub_key",
100+
{"network": network, "node": node},
101+
)
102+
)
103+
104+
90105
@cli.command()
91106
@click.argument("node", type=int, required=True)
92107
@click.option("--network", default="warnet", show_default=True)

src/scenarios/ln_init.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from scenarios.utils import ensure_miner
66
from warnet.test_framework_bridge import WarnetTestFramework
7-
from warnet.utils import channel_match
87

98

109
def cli_help():
@@ -51,7 +50,7 @@ def funded_lnnodes():
5150
for tank in self.warnet.tanks:
5251
if tank.lnnode is None:
5352
continue
54-
if int(tank.lnnode.get_wallet_balance()["confirmed_balance"]) < (split * 100000000):
53+
if int(tank.lnnode.get_wallet_balance()) < (split * 100000000):
5554
return False
5655
return True
5756

@@ -117,13 +116,13 @@ def funded_lnnodes():
117116
self.log.info(f"Waiting for graph gossip sync, LN nodes remaining: {ln_nodes_gossip}")
118117
for index in ln_nodes_gossip:
119118
lnnode = self.warnet.tanks[index].lnnode
120-
my_edges = len(lnnode.lncli("describegraph")["edges"])
121-
my_nodes = len(lnnode.lncli("describegraph")["nodes"])
122-
if my_edges == len(chan_opens) and my_nodes == len(ln_nodes):
119+
count_channels = len(lnnode.get_graph_channels())
120+
count_graph_nodes = len(lnnode.get_graph_nodes())
121+
if count_channels == len(chan_opens) and count_graph_nodes == len(ln_nodes):
123122
ln_nodes_gossip.remove(index)
124123
else:
125124
self.log.info(
126-
f" node {index} not synced (channels: {my_edges}/{len(chan_opens)}, nodes: {my_nodes}/{len(ln_nodes)})"
125+
f" node {index} not synced (channels: {count_channels}/{len(chan_opens)}, nodes: {count_graph_nodes}/{len(ln_nodes)})"
127126
)
128127
sleep(5)
129128

@@ -142,14 +141,31 @@ def funded_lnnodes():
142141
score = 0
143142
for tank_index, me in enumerate(ln_nodes):
144143
you = (tank_index + 1) % len(ln_nodes)
145-
my_channels = self.warnet.tanks[me].lnnode.lncli("describegraph")["edges"]
146-
your_channels = self.warnet.tanks[you].lnnode.lncli("describegraph")["edges"]
144+
my_channels = self.warnet.tanks[me].lnnode.get_graph_channels()
145+
your_channels = self.warnet.tanks[you].lnnode.get_graph_channels()
147146
match = True
148-
for chan_index, my_chan in enumerate(my_channels):
149-
your_chan = your_channels[chan_index]
150-
if not channel_match(my_chan, your_chan, allow_flip=False):
147+
for _chan_index, my_chan in enumerate(my_channels):
148+
your_chan = [
149+
chan
150+
for chan in your_channels
151+
if chan.short_chan_id == my_chan.short_chan_id
152+
][0]
153+
if not your_chan:
154+
print(f"Channel policy missing for channel: {my_chan.short_chan_id}")
155+
match = False
156+
break
157+
158+
try:
159+
if not my_chan.channel_match(your_chan):
160+
print(
161+
f"Channel policy doesn't match between tanks {me} & {you}: {my_chan.short_chan_id}"
162+
)
163+
match = False
164+
break
165+
except Exception as e:
166+
print(f"Error comparing channel policies: {e}")
151167
print(
152-
f"Channel policy doesn't match between tanks {me} & {you}: {my_chan['channel_id']}"
168+
f"Channel policy doesn't match between tanks {me} & {you}: {my_chan.short_chan_id}"
153169
)
154170
match = False
155171
break

src/schema/graph_schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"comment": "A string of configure options used when building Bitcoin Core from source code, e.g. '--without-gui --disable-tests'"},
4444
"ln": {
4545
"type": "string",
46-
"comment": "Attach a lightning network node of this implementation (currently only supports 'lnd')"},
46+
"comment": "Attach a lightning network node of this implementation (currently only supports 'lnd' or 'cln')"},
4747
"ln_image": {
4848
"type": "string",
4949
"comment": "Specify a lightning network node image from Dockerhub with the format repository/image:tag"},

src/warnet/cln.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import io
2+
import tarfile
3+
4+
from backend.kubernetes_backend import KubernetesBackend
5+
from warnet.services import ServiceType
6+
from warnet.utils import exponential_backoff, generate_ipv4_addr, handle_json
7+
8+
from .lnchannel import LNChannel
9+
from .lnnode import LNNode
10+
from .status import RunningStatus
11+
12+
CLN_CONFIG_BASE = " ".join(
13+
[
14+
"--network=regtest",
15+
"--database-upgrade=true",
16+
"--bitcoin-retry-timeout=600",
17+
"--bind-addr=0.0.0.0:9735",
18+
"--developer",
19+
"--dev-fast-gossip",
20+
"--log-level=debug",
21+
]
22+
)
23+
24+
25+
class CLNNode(LNNode):
26+
def __init__(self, warnet, tank, backend: KubernetesBackend, options):
27+
self.warnet = warnet
28+
self.tank = tank
29+
self.backend = backend
30+
self.image = options["ln_image"]
31+
self.cb = options["cb_image"]
32+
self.ln_config = options["ln_config"]
33+
self.ipv4 = generate_ipv4_addr(self.warnet.subnet)
34+
self.rpc_port = 10009
35+
self.impl = "cln"
36+
37+
@property
38+
def status(self) -> RunningStatus:
39+
return super().status
40+
41+
@property
42+
def cb_status(self) -> RunningStatus:
43+
return super().cb_status
44+
45+
def get_conf(self, ln_container_name, tank_container_name) -> str:
46+
conf = CLN_CONFIG_BASE
47+
conf += f" --alias={self.tank.index}"
48+
conf += f" --grpc-port={self.rpc_port}"
49+
conf += f" --bitcoin-rpcuser={self.tank.rpc_user}"
50+
conf += f" --bitcoin-rpcpassword={self.tank.rpc_password}"
51+
conf += f" --bitcoin-rpcconnect={tank_container_name}"
52+
conf += f" --bitcoin-rpcport={self.tank.rpc_port}"
53+
conf += f" --announce-addr=dns:{ln_container_name}:9735"
54+
return conf
55+
56+
@exponential_backoff(max_retries=20, max_delay=300)
57+
@handle_json
58+
def lncli(self, cmd) -> dict:
59+
cli = "lightning-cli"
60+
cmd = f"{cli} --network=regtest {cmd}"
61+
return self.backend.exec_run(self.tank.index, ServiceType.LIGHTNING, cmd)
62+
63+
def getnewaddress(self):
64+
return self.lncli("newaddr")["bech32"]
65+
66+
def get_pub_key(self):
67+
res = self.lncli("getinfo")
68+
return res["id"]
69+
70+
def getURI(self):
71+
res = self.lncli("getinfo")
72+
if len(res["address"]) < 1:
73+
return None
74+
return f'{res["id"]}@{res["address"][0]["address"]}:{res["address"][0]["port"]}'
75+
76+
def get_wallet_balance(self) -> int:
77+
res = self.lncli("listfunds")
78+
return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000)
79+
80+
# returns the channel point in the form txid:output_index
81+
def open_channel_to_tank(self, index: int, channel_open_data: str) -> str:
82+
tank = self.warnet.tanks[index]
83+
[pubkey, host] = tank.lnnode.getURI().split("@")
84+
res = self.lncli(f"fundchannel id={pubkey} {channel_open_data}")
85+
return f"{res['txid']}:{res['outnum']}"
86+
87+
def update_channel_policy(self, chan_point: str, policy: str) -> str:
88+
return self.lncli(f"setchannel {chan_point} {policy}")
89+
90+
def get_graph_nodes(self) -> list[str]:
91+
return list(n["nodeid"] for n in self.lncli("listnodes")["nodes"])
92+
93+
def get_graph_channels(self) -> list[LNChannel]:
94+
cln_channels = self.lncli("listchannels")["channels"]
95+
# CLN lists channels twice, once for each direction. This finds the unique channel ids.
96+
short_channel_ids = {chan["short_channel_id"]: chan for chan in cln_channels}.keys()
97+
channels = []
98+
for short_channel_id in short_channel_ids:
99+
node1, node2 = (
100+
chans for chans in cln_channels if chans["short_channel_id"] == short_channel_id
101+
)
102+
channels.append(self.lnchannel_from_json(node1, node2))
103+
return channels
104+
105+
@staticmethod
106+
def lnchannel_from_json(node1: object, node2: object) -> LNChannel:
107+
assert node1["short_channel_id"] == node2["short_channel_id"]
108+
assert node1["direction"] != node2["direction"]
109+
return LNChannel(
110+
node1_pub=node1["source"],
111+
node2_pub=node2["source"],
112+
capacity_msat=node1["amount_msat"],
113+
short_chan_id=node1["short_channel_id"],
114+
node1_min_htlc=node1["htlc_minimum_msat"],
115+
node2_min_htlc=node2["htlc_minimum_msat"],
116+
node1_max_htlc=node1["htlc_maximum_msat"],
117+
node2_max_htlc=node2["htlc_maximum_msat"],
118+
node1_base_fee_msat=node1["base_fee_millisatoshi"],
119+
node2_base_fee_msat=node2["base_fee_millisatoshi"],
120+
node1_fee_rate_milli_msat=node1["fee_per_millionth"],
121+
node2_fee_rate_milli_msat=node2["fee_per_millionth"],
122+
)
123+
124+
def get_peers(self) -> list[str]:
125+
return list(p["id"] for p in self.lncli("listpeers")["peers"])
126+
127+
def connect_to_tank(self, index):
128+
return super().connect_to_tank(index)
129+
130+
def generate_cli_command(self, command: list[str]):
131+
network = f"--network={self.tank.warnet.bitcoin_network}"
132+
cmd = f"{network} {' '.join(command)}"
133+
cmd = f"lightning-cli {cmd}"
134+
return cmd
135+
136+
def export(self, config: object, tar_file):
137+
# Retrieve the credentials
138+
ca_cert = self.backend.get_file(
139+
self.tank.index,
140+
ServiceType.LIGHTNING,
141+
"/root/.lightning/regtest/ca.pem",
142+
)
143+
client_cert = self.backend.get_file(
144+
self.tank.index,
145+
ServiceType.LIGHTNING,
146+
"/root/.lightning/regtest/client.pem",
147+
)
148+
client_key = self.backend.get_file(
149+
self.tank.index,
150+
ServiceType.LIGHTNING,
151+
"/root/.lightning/regtest/client-key.pem",
152+
)
153+
name = f"ln-{self.tank.index}"
154+
ca_cert_filename = f"{name}_ca_cert.pem"
155+
client_cert_filename = f"{name}_client_cert.pem"
156+
client_key_filename = f"{name}_client_key.pem"
157+
host = self.backend.get_lnnode_hostname(self.tank.index)
158+
159+
# Add the files to the in-memory tar archive
160+
tarinfo1 = tarfile.TarInfo(name=ca_cert_filename)
161+
tarinfo1.size = len(ca_cert)
162+
fileobj1 = io.BytesIO(ca_cert)
163+
tar_file.addfile(tarinfo=tarinfo1, fileobj=fileobj1)
164+
tarinfo2 = tarfile.TarInfo(name=client_cert_filename)
165+
tarinfo2.size = len(client_cert)
166+
fileobj2 = io.BytesIO(client_cert)
167+
tar_file.addfile(tarinfo=tarinfo2, fileobj=fileobj2)
168+
tarinfo3 = tarfile.TarInfo(name=client_key_filename)
169+
tarinfo3.size = len(client_key)
170+
fileobj3 = io.BytesIO(client_key)
171+
tar_file.addfile(tarinfo=tarinfo3, fileobj=fileobj3)
172+
173+
config["nodes"].append(
174+
{
175+
"id": name,
176+
"address": f"https://{host}:{self.rpc_port}",
177+
"ca_cert": f"/simln/{ca_cert_filename}",
178+
"client_cert": f"/simln/{client_cert_filename}",
179+
"client_key": f"/simln/{client_key_filename}",
180+
}
181+
)

0 commit comments

Comments
 (0)