Skip to content

Commit e137246

Browse files
committed
Move LND out of commander and refactor ln_init for new architecture
1 parent 3778951 commit e137246

File tree

6 files changed

+563
-217
lines changed

6 files changed

+563
-217
lines changed

resources/scenarios/commander.py

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import argparse
22
import base64
33
import configparser
4-
import http.client
54
import json
65
import logging
76
import os
87
import pathlib
98
import random
109
import signal
11-
import ssl
1210
import sys
1311
import tempfile
1412
from typing import Dict
1513

1614
from kubernetes import client, config
15+
from ln_framework.ln import LND
1716
from test_framework.authproxy import AuthServiceProxy
1817
from test_framework.p2p import NetworkThread
1918
from test_framework.test_framework import (
@@ -24,13 +23,6 @@
2423
from test_framework.test_node import TestNode
2524
from test_framework.util import PortSeed, get_rpc_proxy
2625

27-
# hard-coded deterministic lnd credentials
28-
ADMIN_MACAROON_HEX = "0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6"
29-
# Don't worry about lnd's self-signed certificates
30-
INSECURE_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
31-
INSECURE_CONTEXT.check_hostname = False
32-
INSECURE_CONTEXT.verify_mode = ssl.CERT_NONE
33-
3426
# Figure out what namespace we are in
3527
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace") as f:
3628
NAMESPACE = f.read().strip()
@@ -55,6 +47,7 @@
5547
"rpc_port": int(pod.metadata.labels["RPCPort"]),
5648
"rpc_user": "user",
5749
"rpc_password": pod.metadata.labels["rpcpassword"],
50+
"init_peers": pod.metadata.annotations["init_peers"],
5851
}
5952
)
6053

@@ -80,45 +73,6 @@ def auth_proxy_request(self, method, path, postdata):
8073
AuthServiceProxy._request = auth_proxy_request
8174

8275

83-
class LND:
84-
def __init__(self, pod_name):
85-
self.conn = http.client.HTTPSConnection(
86-
host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT
87-
)
88-
89-
def get(self, uri):
90-
self.conn.request(
91-
method="GET", url=uri, headers={"Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX}
92-
)
93-
return self.conn.getresponse().read().decode("utf8")
94-
95-
def post(self, uri, data):
96-
body = json.dumps(data)
97-
self.conn.request(
98-
method="POST",
99-
url=uri,
100-
body=body,
101-
headers={
102-
"Content-Type": "application/json",
103-
"Content-Length": str(len(body)),
104-
"Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX,
105-
},
106-
)
107-
# Stream output, otherwise we get a timeout error
108-
res = self.conn.getresponse()
109-
stream = ""
110-
while True:
111-
try:
112-
data = res.read(1)
113-
if len(data) == 0:
114-
break
115-
else:
116-
stream += data.decode("utf8")
117-
except Exception:
118-
break
119-
return stream
120-
121-
12276
class Commander(BitcoinTestFramework):
12377
# required by subclasses of BitcoinTestFramework
12478
def set_test_params(self):
@@ -139,6 +93,13 @@ def ensure_miner(node):
13993
def hex_to_b64(hex):
14094
return base64.b64encode(bytes.fromhex(hex)).decode()
14195

96+
@staticmethod
97+
def b64_to_hex(b64, reverse=False):
98+
if reverse:
99+
return base64.b64decode(b64)[::-1].hex()
100+
else:
101+
return base64.b64decode(b64).hex()
102+
142103
def handle_sigterm(self, signum, frame):
143104
print("SIGTERM received, stopping...")
144105
self.shutdown()
@@ -193,6 +154,7 @@ def setup(self):
193154
coveragedir=self.options.coveragedir,
194155
)
195156
node.rpc_connected = True
157+
node.init_peers = int(tank["init_peers"])
196158

197159
self.nodes.append(node)
198160
self.tanks[tank["tank"]] = node

resources/scenarios/ln_framework/__init__.py

Whitespace-only changes.
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import http.client
2+
import json
3+
import ssl
4+
import time
5+
6+
# hard-coded deterministic lnd credentials
7+
ADMIN_MACAROON_HEX = "0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6"
8+
# Don't worry about lnd's self-signed certificates
9+
INSECURE_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
10+
INSECURE_CONTEXT.check_hostname = False
11+
INSECURE_CONTEXT.verify_mode = ssl.CERT_NONE
12+
13+
14+
# https://github.com/lightningcn/lightning-rfc/blob/master/07-routing-gossip.md#the-channel_update-message
15+
# We use the field names as written in the BOLT as our canonical, internal field names.
16+
# In LND, Policy objects returned by DescribeGraph have completely different labels
17+
# than policy objects expected by the UpdateChannelPolicy API, and neither
18+
# of these are the names used in the BOLT...
19+
class Policy:
20+
def __init__(
21+
self,
22+
cltv_expiry_delta: int,
23+
htlc_minimum_msat: int,
24+
fee_base_msat: int,
25+
fee_proportional_millionths: int,
26+
htlc_maximum_msat: int,
27+
):
28+
self.cltv_expiry_delta = cltv_expiry_delta
29+
self.htlc_minimum_msat = htlc_minimum_msat
30+
self.fee_base_msat = fee_base_msat
31+
self.fee_proportional_millionths = fee_proportional_millionths
32+
self.htlc_maximum_msat = htlc_maximum_msat
33+
34+
@classmethod
35+
def from_lnd_describegraph(cls, policy: dict):
36+
return cls(
37+
cltv_expiry_delta=int(policy.get("time_lock_delta")),
38+
htlc_minimum_msat=int(policy.get("min_htlc")),
39+
fee_base_msat=int(policy.get("fee_base_msat")),
40+
fee_proportional_millionths=int(policy.get("fee_rate_milli_msat")),
41+
htlc_maximum_msat=int(policy.get("max_htlc_msat")),
42+
)
43+
44+
@classmethod
45+
def from_dict(cls, policy: dict):
46+
return cls(
47+
cltv_expiry_delta=policy.get("cltv_expiry_delta"),
48+
htlc_minimum_msat=policy.get("htlc_minimum_msat"),
49+
fee_base_msat=policy.get("fee_base_msat"),
50+
fee_proportional_millionths=policy.get("fee_proportional_millionths"),
51+
htlc_maximum_msat=policy.get("htlc_maximum_msat"),
52+
)
53+
54+
def to_dict(self):
55+
return {
56+
"cltv_expiry_delta": self.cltv_expiry_delta,
57+
"htlc_minimum_msat": self.htlc_minimum_msat,
58+
"fee_base_msat": self.fee_base_msat,
59+
"fee_proportional_millionths": self.fee_proportional_millionths,
60+
"htlc_maximum_msat": self.htlc_maximum_msat,
61+
}
62+
63+
def to_lnd_chanpolicy(self, capacity):
64+
# LND requires a 1% reserve
65+
reserve = ((capacity * 99) // 100) * 1000
66+
return {
67+
"time_lock_delta": self.cltv_expiry_delta,
68+
"min_htlc_msat": self.htlc_minimum_msat,
69+
"base_fee_msat": self.fee_base_msat,
70+
"fee_rate_ppm": self.fee_proportional_millionths,
71+
"max_htlc_msat": min(self.htlc_maximum_msat, reserve),
72+
"min_htlc_msat_specified": True,
73+
}
74+
75+
76+
class LND:
77+
def __init__(self, pod_name):
78+
self.name = pod_name
79+
self.conn = http.client.HTTPSConnection(
80+
host=pod_name, port=8080, timeout=5, context=INSECURE_CONTEXT
81+
)
82+
83+
def get(self, uri):
84+
while True:
85+
try:
86+
self.conn.request(
87+
method="GET",
88+
url=uri,
89+
headers={"Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX, "Connection": "close"},
90+
)
91+
return self.conn.getresponse().read().decode("utf8")
92+
except Exception:
93+
time.sleep(1)
94+
95+
def post(self, uri, data):
96+
body = json.dumps(data)
97+
attempt = 0
98+
while True:
99+
attempt += 1
100+
try:
101+
self.conn.request(
102+
method="POST",
103+
url=uri,
104+
body=body,
105+
headers={
106+
"Content-Type": "application/json",
107+
"Content-Length": str(len(body)),
108+
"Grpc-Metadata-macaroon": ADMIN_MACAROON_HEX,
109+
"Connection": "close",
110+
},
111+
)
112+
# Stream output, otherwise we get a timeout error
113+
res = self.conn.getresponse()
114+
stream = ""
115+
while True:
116+
try:
117+
data = res.read(1)
118+
if len(data) == 0:
119+
break
120+
else:
121+
stream += data.decode("utf8")
122+
except Exception:
123+
break
124+
return stream
125+
except Exception:
126+
time.sleep(1)
127+
128+
def newaddress(self):
129+
res = self.get("/v1/newaddress")
130+
return json.loads(res)
131+
132+
def walletbalance(self):
133+
res = self.get("/v1/balance/blockchain")
134+
return int(json.loads(res)["confirmed_balance"])
135+
136+
def uri(self):
137+
res = self.get("/v1/getinfo")
138+
info = json.loads(res)
139+
if "uris" not in info or len(info["uris"]) == 0:
140+
return None
141+
return info["uris"][0]
142+
143+
def connect(self, target_uri):
144+
pk, host = target_uri.split("@")
145+
res = self.post("/v1/peers", data={"addr": {"pubkey": pk, "host": host}})
146+
return json.loads(res)
147+
148+
def channel(self, pk, capacity, push_amt, fee_rate):
149+
res = self.post(
150+
"/v1/channels/stream",
151+
data={
152+
"local_funding_amount": capacity,
153+
"push_sat": push_amt,
154+
"node_pubkey": pk,
155+
"sat_per_vbyte": fee_rate,
156+
},
157+
)
158+
return json.loads(res)
159+
160+
def update(self, txid_hex: str, policy: dict, capacity: int):
161+
ln_policy = Policy.from_dict(policy).to_lnd_chanpolicy(capacity)
162+
data = {"chan_point": {"funding_txid_str": txid_hex, "output_index": 0}, **ln_policy}
163+
res = self.post(
164+
"/v1/chanpolicy",
165+
# Policy objects returned by DescribeGraph have
166+
# completely different labels than policy objects expected
167+
# by the UpdateChannelPolicy API.
168+
data=data,
169+
)
170+
return json.loads(res)
171+
172+
def graph(self):
173+
res = self.get("/v1/graph")
174+
return json.loads(res)

0 commit comments

Comments
 (0)