Skip to content

Commit cda355b

Browse files
committed
Add ln_mixed node test
Update documentation to address CLN support Make ln_framework/ln.py work in commander or local device Add CLN support to warnet/ln.py Add short sleep between thread starts to improve stability
1 parent ff9d32e commit cda355b

File tree

12 files changed

+320
-60
lines changed

12 files changed

+320
-60
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jobs:
4141
- graph_test.py
4242
- logging_test.py
4343
- ln_basic_test.py
44+
- ln_mixed_test.py
4445
- ln_test.py
4546
- rpc_test.py
4647
- services_test.py

docs/plugins.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ plugins:
3434
postDeploy: # Plugins will run after all the `deploy` code has run.
3535
simln:
3636
entrypoint: "../plugins/simln"
37-
activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-lnd", "interval_secs": 1, "amount_msat": 2000}]'
37+
activity: '[{"source": "tank-0003-lnd", "destination": "tank-0005-cln", "interval_secs": 1, "amount_msat": 2000}]'
3838
hello:
3939
entrypoint: "../plugins/hello"
4040
podName: "hello-post-deploy"

docs/scenarios.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,9 @@ Total Tanks: 6 | Active Scenarios: 0
7272
## Running a custom scenario
7373

7474
You can write your own scenario file and run it in the same way.
75+
76+
## Scenarios with lightning nodes
77+
78+
When defining network.yaml all lnd nodes should be indexed in the same block before any cln nodes otherwise node responsiveness causes the expected index to get out of order with actual regardless of how the channels are opened
79+
Review `test/data/ln_mixed/network.yaml` for an example
80+

resources/plugins/simln/README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,29 @@ SimLN also requires access details for each node; however, the SimLN plugin will
1313

1414
```` JSON
1515
{
16-
"id": <node_id>,
16+
"id": <node_id>-lnd,
1717
"address": https://<ip:port or domain:port>,
1818
"macaroon": <path_to_selected_macaroon>,
1919
"cert": <path_to_tls_cert>
2020
}
2121
````
22+
SimLN also supports Core Lightning (CLN). CLN nodes connection details are transfered from the CLN node to SimLN node during launch-activity processing.
23+
```` JSON
24+
{
25+
"id": <node_id>-cln,
26+
"address": https://<domain:port>,
27+
"ca_cert": /working/<node_id>-cln-ca.pem,
28+
"client_cert": /working/<node_id>-cln-client.pem,
29+
"client_key": /working/<node_id>-cln-client-key.pem
30+
}
31+
````
2232

23-
Since SimLN already has access to those LND connection details, it means you can focus on the "activity" definitions.
33+
Since SimLN already has access to those LND and CLN connection details, it means you can focus on the "activity" definitions.
2434

2535
### Launch activity definitions from the command line
2636
The SimLN plugin takes "activity" definitions like so:
2737

28-
`./simln/plugin.py launch-activity '[{\"source\": \"tank-0003-ln\", \"destination\": \"tank-0005-ln\", \"interval_secs\": 1, \"amount_msat\": 2000}]'"''`
38+
`./simln/plugin.py launch-activity '[{\"source\": \"tank-0003-lnd\", \"destination\": \"tank-0005-lnd\", \"interval_secs\": 1, \"amount_msat\": 2000}]'"''`
2939

3040
### Launch activity definitions from within `network.yaml`
3141
When you initialize a new Warnet network, Warnet will create a new `network.yaml` file. If your `network.yaml` file includes lightning nodes, then you can use SimLN to produce activity between those nodes like this:

resources/plugins/simln/plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def _generate_activity_json(activity: Optional[list[dict]]) -> str:
168168
ln_name = i.metadata.name
169169
port = 10009
170170
node = {"id": ln_name}
171-
if "cln" in ln_name:
171+
if "-cln" in ln_name:
172172
port = 9736
173173
node["ca_cert"] = f"/working/{ln_name}-ca.pem"
174174
node["client_cert"] = f"/working/{ln_name}-client.pem"

resources/scenarios/ln_framework/ln.py

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
import http.client
33
import json
4+
import os
45
import ssl
56
from abc import ABC, abstractmethod
67
from time import sleep
@@ -19,7 +20,10 @@
1920

2021
# execute kubernetes command
2122
def run_command(name, command: list[str], namespace: Optional[str] = "default") -> str:
22-
config.load_incluster_config()
23+
if os.getenv("KUBERNETES_SERVICE_HOST") and os.getenv("KUBERNETES_SERVICE_PORT"):
24+
config.load_incluster_config()
25+
else:
26+
config.load_kube_config()
2327
sclient = client.CoreV1Api()
2428
resp = stream(
2529
sclient.connect_get_namespaced_pod_exec,
@@ -209,7 +213,7 @@ def uri(self):
209213
return None
210214
return f"{res['id']}@{res['address'][0]['address']}:{res['address'][0]['port']}"
211215

212-
def walletbalance(self, max_tries=2):
216+
def walletbalance(self, max_tries=2) -> int:
213217
attempt = 0
214218
while attempt < max_tries:
215219
attempt += 1
@@ -221,6 +225,18 @@ def walletbalance(self, max_tries=2):
221225
return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000)
222226
return 0
223227

228+
def channelbalance(self, max_tries=2) -> int:
229+
attempt = 0
230+
while attempt < max_tries:
231+
attempt += 1
232+
response = self.rpc("listfunds")
233+
if not response:
234+
sleep(2)
235+
continue
236+
res = json.loads(response)
237+
return int(sum(o["our_amount_msat"] for o in res["channels"]) / 1000)
238+
return 0
239+
224240
def connect(self, target_uri, max_tries=5) -> dict:
225241
attempt = 0
226242
while attempt < max_tries:
@@ -263,6 +279,23 @@ def channel(self, pk, capacity, push_amt, fee_rate, max_tries=5) -> dict:
263279
sleep(2)
264280
return None
265281

282+
def createinvoice(self, sats, label, description="new invoice") -> str:
283+
response = self.rpc("invoice", [sats * 1000, label, description])
284+
if response:
285+
res = json.loads(response)
286+
return res["bolt11"]
287+
return None
288+
289+
def payinvoice(self, payment_request) -> str:
290+
response = self.rpc("pay", [payment_request])
291+
if response:
292+
res = json.loads(response)
293+
if "code" in res:
294+
return res["message"]
295+
else:
296+
return res["payment_hash"]
297+
return None
298+
266299
def graph(self, max_tries=2) -> dict:
267300
attempt = 0
268301
while attempt < max_tries:
@@ -304,7 +337,13 @@ def __init__(self, pod_name, logger):
304337
}
305338
self.impl = "lnd"
306339

340+
def reset_connection(self):
341+
self.conn = http.client.HTTPSConnection(
342+
host=self.name, port=8080, timeout=5, context=INSECURE_CONTEXT
343+
)
344+
307345
def get(self, uri):
346+
attempt = 0
308347
while True:
309348
try:
310349
self.conn.request(
@@ -313,7 +352,12 @@ def get(self, uri):
313352
headers=self.headers,
314353
)
315354
return self.conn.getresponse().read().decode("utf8")
316-
except Exception:
355+
except Exception as e:
356+
self.reset_connection()
357+
attempt += 1
358+
if attempt > 5:
359+
self.log.error(f"Error LND POST, Abort: {e}")
360+
return None
317361
sleep(1)
318362

319363
def post(self, uri, data):
@@ -323,7 +367,6 @@ def post(self, uri, data):
323367
post_header["Content-Type"] = "application/json"
324368
attempt = 0
325369
while True:
326-
attempt += 1
327370
try:
328371
self.conn.request(
329372
method="POST",
@@ -344,7 +387,12 @@ def post(self, uri, data):
344387
except Exception:
345388
break
346389
return stream
347-
except Exception:
390+
except Exception as e:
391+
self.reset_connection()
392+
attempt += 1
393+
if attempt > 5:
394+
self.log.error(f"Error LND POST, Abort: {e}")
395+
return None
348396
sleep(1)
349397

350398
def newaddress(self, max_tries=10):
@@ -362,10 +410,14 @@ def newaddress(self, max_tries=10):
362410
sleep(1)
363411
return False, ""
364412

365-
def walletbalance(self):
413+
def walletbalance(self) -> int:
366414
res = self.get("/v1/balance/blockchain")
367415
return int(json.loads(res)["confirmed_balance"])
368416

417+
def channelbalance(self) -> int:
418+
res = self.get("/v1/balance/channels")
419+
return int(json.loads(res)["balance"])
420+
369421
def uri(self):
370422
res = self.get("/v1/getinfo")
371423
info = json.loads(res)
@@ -420,6 +472,28 @@ def update(self, txid_hex: str, policy: dict, capacity: int):
420472
)
421473
return json.loads(res)
422474

475+
def createinvoice(self, sats, label, description="new invoice") -> str:
476+
b64_desc = base64.b64encode(description.encode("utf-8"))
477+
response = self.post(
478+
"/v1/invoices", data={"value": sats, "memo": label, "description_hash": b64_desc}
479+
)
480+
if response:
481+
res = json.loads(response)
482+
return res["payment_request"]
483+
return None
484+
485+
def payinvoice(self, payment_request) -> str:
486+
response = self.post(
487+
"/v1/channels/transaction-stream", data={"payment_request": payment_request}
488+
)
489+
if response:
490+
res = json.loads(response)
491+
if "payment_error" in res:
492+
return res["payment_error"]
493+
else:
494+
return res["payment_hash"]
495+
return None
496+
423497
def graph(self):
424498
res = self.get("/v1/graph")
425499
return json.loads(res)

resources/scenarios/ln_init.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ def connect_ln(self, pair):
185185
threading.Thread(target=connect_ln, args=(self, pair)) for pair in connections
186186
]
187187
for thread in p2p_threads:
188+
sleep(0.25)
188189
thread.start()
189190

190191
all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in p2p_threads)
@@ -258,6 +259,7 @@ def open_channel(self, ch, fee_rate):
258259
assert index == ch["id"]["index"], "Channel ID indexes are not consecutive"
259260
assert fee_rate >= 1, "Too many TXs in block, out of fee range"
260261
t = threading.Thread(target=open_channel, args=(self, ch, fee_rate))
262+
sleep(0.25)
261263
t.start()
262264
ch_threads.append(t)
263265

@@ -305,6 +307,7 @@ def ln_all_chs(self, ln):
305307

306308
ch_ann_threads = [threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in ln_nodes]
307309
for thread in ch_ann_threads:
310+
sleep(0.25)
308311
thread.start()
309312

310313
all(thread.join(timeout=THREAD_JOIN_TIMEOUT * 2) is None for thread in ch_ann_threads)
@@ -335,6 +338,7 @@ def update_policy(self, ln, txid_hex, policy, capacity):
335338
ch["capacity"],
336339
),
337340
)
341+
sleep(0.25)
338342
ts.start()
339343
update_threads.append(ts)
340344
if "target_policy" in ch:
@@ -348,6 +352,7 @@ def update_policy(self, ln, txid_hex, policy, capacity):
348352
ch["capacity"],
349353
),
350354
)
355+
sleep(0.25)
351356
tt.start()
352357
update_threads.append(tt)
353358
count = len(update_threads)
@@ -408,6 +413,7 @@ def matching_graph(self, expected, ln):
408413
threading.Thread(target=matching_graph, args=(self, expected, ln)) for ln in ln_nodes
409414
]
410415
for thread in policy_threads:
416+
sleep(0.25)
411417
thread.start()
412418

413419
all(thread.join(timeout=THREAD_JOIN_TIMEOUT) is None for thread in policy_threads)

src/warnet/ln.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ def _rpc(pod_name: str, method: str, params: str = "", namespace: Optional[str]
3131
pod = get_pod(pod_name)
3232
namespace = get_default_namespace_or(namespace)
3333
chain = pod.metadata.labels["chain"]
34-
cmd = f"kubectl -n {namespace} exec {pod_name} -- lncli --network {chain} {method} {' '.join(map(str, params))}"
34+
ln_client = "lncli"
35+
if "-cln" in pod_name:
36+
ln_client = "lightning-cli"
37+
cmd = f"kubectl -n {namespace} exec {pod_name} -- {ln_client} --network {chain} {method} {' '.join(map(str, params))}"
3538
return run_command(cmd)
3639

3740

@@ -48,7 +51,10 @@ def pubkey(
4851

4952
def _pubkey(pod: str):
5053
info = _rpc(pod, "getinfo")
51-
return json.loads(info)["identity_pubkey"]
54+
pubkey_key = "identity_pubkey"
55+
if "-cln" in pod:
56+
pubkey_key = "id"
57+
return json.loads(info)[pubkey_key]
5258

5359

5460
@ln.command()
@@ -64,8 +70,11 @@ def host(
6470

6571
def _host(pod):
6672
info = _rpc(pod, "getinfo")
67-
uris = json.loads(info)["uris"]
68-
if uris and len(uris) >= 0:
69-
return uris[0].split("@")[1]
73+
if "-cln" in pod:
74+
return json.loads(info)["alias"]
7075
else:
71-
return ""
76+
uris = json.loads(info)["uris"]
77+
if uris and len(uris) >= 0:
78+
return uris[0].split("@")[1]
79+
else:
80+
return ""

test/data/cln/network.yaml

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)