Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions src/backend/kubernetes_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@ def ln_cli(self, tank: Tank, command: list[str]):
self.log.debug(f"Running lncli {cmd=:} on {tank.index=:}")
return self.exec_run(tank.index, ServiceType.LIGHTNING, cmd)

def ln_pub_key(self, tank) -> str:
if tank.lnnode is None:
raise Exception("No LN node configured for tank")
self.log.debug(f"Getting pub key for tank {tank.index}")
return tank.lnnode.get_pub_key()

def get_bitcoin_cli(self, tank: Tank, method: str, params=None):
if params:
cmd = f"bitcoin-cli -regtest -rpcuser={tank.rpc_user} -rpcport={tank.rpc_port} -rpcpassword={tank.rpc_password} {method} {' '.join(map(str, params))}"
Expand Down Expand Up @@ -468,14 +474,21 @@ def remove_prometheus_service_monitors(self, tanks):
def get_lnnode_hostname(self, index: int) -> str:
return f"{self.get_service_name(index, ServiceType.LIGHTNING)}.{self.namespace}"

def create_lnd_container(
self, tank, bitcoind_service_name, volume_mounts
) -> client.V1Container:
def create_ln_container(self, tank, bitcoind_service_name, volume_mounts) -> client.V1Container:
# These args are appended to the Dockerfile `ENTRYPOINT ["lnd"]`
bitcoind_rpc_host = f"{bitcoind_service_name}.{self.namespace}"
lightning_dns = self.get_lnnode_hostname(tank.index)
args = tank.lnnode.get_conf(lightning_dns, bitcoind_rpc_host)
self.log.debug(f"Creating lightning container for tank {tank.index} using {args=:}")
lightning_ready_probe = ""
if tank.lnnode.impl == "lnd":
lightning_ready_probe = "lncli --network=regtest getinfo"
elif tank.lnnode.impl == "cln":
lightning_ready_probe = "lightning-cli --network=regtest getinfo"
else:
raise Exception(
f"Lightning node implementation {tank.lnnode.impl} for tank {tank.index} not supported"
)
lightning_container = client.V1Container(
name=LN_CONTAINER_NAME,
image=tank.lnnode.image,
Expand All @@ -486,12 +499,10 @@ def create_lnd_container(
readiness_probe=client.V1Probe(
failure_threshold=1,
success_threshold=3,
initial_delay_seconds=1,
initial_delay_seconds=10,
period_seconds=2,
timeout_seconds=2,
_exec=client.V1ExecAction(
command=["/bin/sh", "-c", "lncli --network=regtest getinfo"]
),
_exec=client.V1ExecAction(command=["/bin/sh", "-c", lightning_ready_probe]),
),
security_context=client.V1SecurityContext(
privileged=True,
Expand Down Expand Up @@ -681,7 +692,7 @@ def deploy_pods(self, warnet):
raise e
self.client.create_namespaced_service(namespace=self.namespace, body=bitcoind_service)

# Create and deploy LND pod
# Create and deploy a lightning pod
if tank.lnnode:
conts = []
vols = []
Expand All @@ -700,9 +711,9 @@ def deploy_pods(self, warnet):
)
# Add circuit breaker container
conts.append(self.create_circuitbreaker_container(tank, volume_mounts))
# Add lnd container
# Add lightning container
conts.append(
self.create_lnd_container(tank, bitcoind_service.metadata.name, volume_mounts)
self.create_ln_container(tank, bitcoind_service.metadata.name, volume_mounts)
)
# Put it all together in a pod
lnd_pod = self.create_pod_object(
Expand Down
15 changes: 15 additions & 0 deletions src/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ def lncli(node: int, command: tuple, network: str):
)


@cli.command(context_settings={"ignore_unknown_options": True})
@click.argument("node", type=int)
@click.option("--network", default="warnet", show_default=True, type=str)
def ln_pub_key(node: int, network: str):
"""
Get lightning node pub key on <node> in [network]
"""
print(
rpc_call(
"tank_ln_pub_key",
{"network": network, "node": node},
)
)


@cli.command()
@click.argument("node", type=int, required=True)
@click.option("--network", default="warnet", show_default=True)
Expand Down
39 changes: 28 additions & 11 deletions src/scenarios/ln_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def funded_lnnodes():
for tank in self.warnet.tanks:
if tank.lnnode is None:
continue
if int(tank.lnnode.get_wallet_balance()["confirmed_balance"]) < (split * 100000000):
if int(tank.lnnode.get_wallet_balance()) < (split * 100000000):
return False
return True

Expand Down Expand Up @@ -117,13 +117,13 @@ def funded_lnnodes():
self.log.info(f"Waiting for graph gossip sync, LN nodes remaining: {ln_nodes_gossip}")
for index in ln_nodes_gossip:
lnnode = self.warnet.tanks[index].lnnode
my_edges = len(lnnode.lncli("describegraph")["edges"])
my_nodes = len(lnnode.lncli("describegraph")["nodes"])
if my_edges == len(chan_opens) and my_nodes == len(ln_nodes):
count_channels = len(lnnode.get_graph_channels())
count_graph_nodes = len(lnnode.get_graph_nodes())
if count_channels == len(chan_opens) and count_graph_nodes == len(ln_nodes):
ln_nodes_gossip.remove(index)
else:
self.log.info(
f" node {index} not synced (channels: {my_edges}/{len(chan_opens)}, nodes: {my_nodes}/{len(ln_nodes)})"
f" node {index} not synced (channels: {count_channels}/{len(chan_opens)}, nodes: {count_graph_nodes}/{len(ln_nodes)})"
)
sleep(5)

Expand All @@ -142,14 +142,31 @@ def funded_lnnodes():
score = 0
for tank_index, me in enumerate(ln_nodes):
you = (tank_index + 1) % len(ln_nodes)
my_channels = self.warnet.tanks[me].lnnode.lncli("describegraph")["edges"]
your_channels = self.warnet.tanks[you].lnnode.lncli("describegraph")["edges"]
my_channels = self.warnet.tanks[me].lnnode.get_graph_channels()
your_channels = self.warnet.tanks[you].lnnode.get_graph_channels()
match = True
for chan_index, my_chan in enumerate(my_channels):
your_chan = your_channels[chan_index]
if not channel_match(my_chan, your_chan, allow_flip=False):
for _chan_index, my_chan in enumerate(my_channels):
your_chan = [
chan
for chan in your_channels
if chan.short_chan_id == my_chan.short_chan_id
][0]
if not your_chan:
print(f"Channel policy missing for channel: {my_chan.short_chan_id}")
match = False
break

try:
if not channel_match(my_chan, your_chan):
print(
f"Channel policy doesn't match between tanks {me} & {you}: {my_chan.short_chan_id}"
)
match = False
break
except Exception as e:
print(f"Error comparing channel policies: {e}")
print(
f"Channel policy doesn't match between tanks {me} & {you}: {my_chan['channel_id']}"
f"Channel policy doesn't match between tanks {me} & {you}: {my_chan.short_chan_id}"
)
match = False
break
Expand Down
2 changes: 1 addition & 1 deletion src/schema/graph_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"comment": "A string of configure options used when building Bitcoin Core from source code, e.g. '--without-gui --disable-tests'"},
"ln": {
"type": "string",
"comment": "Attach a lightning network node of this implementation (currently only supports 'lnd')"},
"comment": "Attach a lightning network node of this implementation (currently only supports 'lnd' or 'cln')"},
"ln_image": {
"type": "string",
"comment": "Specify a lightning network node image from Dockerhub with the format repository/image:tag"},
Expand Down
178 changes: 178 additions & 0 deletions src/warnet/cln.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import io
import tarfile

from backend.kubernetes_backend import KubernetesBackend
from warnet.services import ServiceType
from warnet.utils import exponential_backoff, generate_ipv4_addr, handle_json

from .lnchannel import LNChannel
from .lnnode import LNNode
from .status import RunningStatus

CLN_CONFIG_BASE = " ".join(
[
"--network=regtest",
"--database-upgrade=true",
"--bitcoin-retry-timeout=600",
"--bind-addr=0.0.0.0:9735",
]
)


class CLNNode(LNNode):
def __init__(self, warnet, tank, backend: KubernetesBackend, options):
self.warnet = warnet
self.tank = tank
self.backend = backend
self.image = options["ln_image"]
self.cb = options["cb_image"]
self.ln_config = options["ln_config"]
self.ipv4 = generate_ipv4_addr(self.warnet.subnet)
self.rpc_port = 10009
self.impl = "cln"

@property
def status(self) -> RunningStatus:
return super().status

@property
def cb_status(self) -> RunningStatus:
return super().cb_status

def get_conf(self, ln_container_name, tank_container_name) -> str:
conf = CLN_CONFIG_BASE
conf += f" --alias={self.tank.index}"
conf += f" --grpc-port={self.rpc_port}"
conf += f" --bitcoin-rpcuser={self.tank.rpc_user}"
conf += f" --bitcoin-rpcpassword={self.tank.rpc_password}"
conf += f" --bitcoin-rpcconnect={tank_container_name}"
conf += f" --bitcoin-rpcport={self.tank.rpc_port}"
conf += f" --announce-addr=dns:{ln_container_name}:9735"
return conf

@exponential_backoff(max_retries=20, max_delay=300)
@handle_json
def lncli(self, cmd) -> dict:
cli = "lightning-cli"
cmd = f"{cli} --network=regtest {cmd}"
return self.backend.exec_run(self.tank.index, ServiceType.LIGHTNING, cmd)

def getnewaddress(self):
return self.lncli("newaddr")["bech32"]

def get_pub_key(self):
res = self.lncli("getinfo")
return res["id"]

def getURI(self):
res = self.lncli("getinfo")
if len(res["address"]) < 1:
return None
return f'{res["id"]}@{res["address"][0]["address"]}:{res["address"][0]["port"]}'

def get_wallet_balance(self) -> int:
res = self.lncli("listfunds")
return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000)

# returns the channel point in the form txid:output_index
def open_channel_to_tank(self, index: int, channel_open_data: str) -> str:
tank = self.warnet.tanks[index]
[pubkey, host] = tank.lnnode.getURI().split("@")
res = self.lncli(f"fundchannel id={pubkey} {channel_open_data}")
return f"{res['txid']}:{res['outnum']}"

def update_channel_policy(self, chan_point: str, policy: str) -> str:
return self.lncli(f"setchannel {chan_point} {policy}")

def get_graph_nodes(self) -> list[str]:
return list(n["nodeid"] for n in self.lncli("listnodes")["nodes"])

def get_graph_channels(self) -> list[dict]:
cln_channels = self.lncli("listchannels")["channels"]
# CLN lists channels twice, once for each direction. This finds the unique channel ids.
short_channel_ids = {chan["short_channel_id"]: chan for chan in cln_channels}.keys()
channels: list[LNChannel] = []
for short_channel_id in short_channel_ids:
channel_1 = [
chans for chans in cln_channels if chans["short_channel_id"] == short_channel_id
][0]
channel_2 = [
chans for chans in cln_channels if chans["short_channel_id"] == short_channel_id
][1]

channels.append(
LNChannel(
node1_pub=channel_1["source"],
node2_pub=channel_2["source"],
capacity_msat=channel_1["amount_msat"],
short_chan_id=channel_1["short_channel_id"],
node1_min_htlc=channel_1["htlc_minimum_msat"],
node2_min_htlc=channel_2["htlc_minimum_msat"],
node1_max_htlc=channel_1["htlc_maximum_msat"],
node2_max_htlc=channel_2["htlc_maximum_msat"],
node1_base_fee_msat=channel_1["base_fee_millisatoshi"],
node2_base_fee_msat=channel_2["base_fee_millisatoshi"],
node1_fee_rate_milli_msat=channel_1["fee_per_millionth"],
node2_fee_rate_milli_msat=channel_2["fee_per_millionth"],
)
)

return channels

def get_peers(self) -> list[str]:
return list(p["id"] for p in self.lncli("listpeers")["peers"])

def connect_to_tank(self, index):
return super().connect_to_tank(index)

def generate_cli_command(self, command: list[str]):
network = f"--network={self.tank.warnet.bitcoin_network}"
cmd = f"{network} {' '.join(command)}"
cmd = f"lightning-cli {cmd}"

def export(self, config: object, tar_file):
# Retrieve the credentials
ca_cert = self.backend.get_file(
self.tank.index,
ServiceType.LIGHTNING,
"/root/.lightning/regtest/ca.pem",
)
client_cert = self.backend.get_file(
self.tank.index,
ServiceType.LIGHTNING,
"/root/.lightning/regtest/client.pem",
)
client_key = self.backend.get_file(
self.tank.index,
ServiceType.LIGHTNING,
"/root/.lightning/regtest/client-key.pem",
)
name = f"ln-{self.tank.index}"
ca_cert_filename = f"{name}_ca_cert.pem"
client_cert_filename = f"{name}_client_cert.pem"
client_key_filename = f"{name}_client_key.pem"
host = self.backend.get_lnnode_hostname(self.tank.index)

# Add the files to the in-memory tar archive
tarinfo1 = tarfile.TarInfo(name=ca_cert_filename)
tarinfo1.size = len(ca_cert)
fileobj1 = io.BytesIO(ca_cert)
tar_file.addfile(tarinfo=tarinfo1, fileobj=fileobj1)
tarinfo2 = tarfile.TarInfo(name=client_cert_filename)
tarinfo2.size = len(client_cert)
fileobj2 = io.BytesIO(client_cert)
tar_file.addfile(tarinfo=tarinfo2, fileobj=fileobj2)
tarinfo3 = tarfile.TarInfo(name=client_key_filename)
tarinfo3.size = len(client_key)
fileobj3 = io.BytesIO(client_key)
tar_file.addfile(tarinfo=tarinfo3, fileobj=fileobj3)

config["nodes"].append(
{
"id": name,
"address": f"https://{host}:{self.rpc_port}",
"ca_cert": f"/simln/{ca_cert_filename}",
"client_cert": f"/simln/{client_cert_filename}",
"client_key": f"/simln/{client_key_filename}",
}
)
46 changes: 46 additions & 0 deletions src/warnet/lnchannel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
class LNChannel:
def __init__(
self,
node1_pub: str,
node2_pub: str,
capacity_msat: int = 0,
short_chan_id: str = "",
node1_min_htlc: int = 0,
node2_min_htlc: int = 0,
node1_max_htlc: int = 0,
node2_max_htlc: int = 0,
node1_base_fee_msat: int = 0,
node2_base_fee_msat: int = 0,
node1_fee_rate_milli_msat: int = 0,
node2_fee_rate_milli_msat: int = 0,
node1_time_lock_delta: int = 0,
node2_time_lock_delta: int = 0,
) -> None:
# Ensure that the node with the lower pubkey is node1
if node1_pub > node2_pub:
node1_pub, node2_pub = node2_pub, node1_pub
node1_min_htlc, node2_min_htlc = node2_min_htlc, node1_min_htlc
node1_max_htlc, node2_max_htlc = node2_max_htlc, node1_max_htlc
node1_base_fee_msat, node2_base_fee_msat = node2_base_fee_msat, node1_base_fee_msat
node1_fee_rate_milli_msat, node2_fee_rate_milli_msat = (
node2_fee_rate_milli_msat,
node1_fee_rate_milli_msat,
)
node1_time_lock_delta, node2_time_lock_delta = (
node2_time_lock_delta,
node1_time_lock_delta,
)
self.node1_pub = node1_pub
self.node2_pub = node2_pub
self.capacity_msat = capacity_msat
self.short_chan_id = short_chan_id
self.node1_min_htlc = node1_min_htlc
self.node2_min_htlc = node2_min_htlc
self.node1_max_htlc = node1_max_htlc
self.node2_max_htlc = node2_max_htlc
self.node1_base_fee_msat = node1_base_fee_msat
self.node2_base_fee_msat = node2_base_fee_msat
self.node1_fee_rate_milli_msat = node1_fee_rate_milli_msat
self.node2_fee_rate_milli_msat = node2_fee_rate_milli_msat
self.node1_time_lock_delta = node1_time_lock_delta
self.node2_time_lock_delta = node2_time_lock_delta
Loading